Compare commits
11 Commits
ios-qr-cod
...
star-proje
| Author | SHA1 | Date | |
|---|---|---|---|
| db4496c57b | |||
| a51fd90659 | |||
| 855448d07a | |||
| 9f1495e185 | |||
| f61cb6eea7 | |||
| d3f54d6bff | |||
| 2bb733a9ea | |||
| 7da6f722f5 | |||
| 475f4d5ce5 | |||
| 24a7cf5eb6 | |||
| da0621c09a |
@@ -617,8 +617,7 @@ The Electron build process follows a multi-stage approach:
|
|||||||
#### **Stage 2: Capacitor Sync**
|
#### **Stage 2: Capacitor Sync**
|
||||||
|
|
||||||
- Copies web assets to Electron app directory
|
- Copies web assets to Electron app directory
|
||||||
- Uses Electron-specific Capacitor configuration (not copied from main config)
|
- Syncs Capacitor configuration and plugins
|
||||||
- Syncs Capacitor plugins for Electron platform
|
|
||||||
- Prepares native module bindings
|
- Prepares native module bindings
|
||||||
|
|
||||||
#### **Stage 3: TypeScript Compile**
|
#### **Stage 3: TypeScript Compile**
|
||||||
|
|||||||
@@ -1,181 +0,0 @@
|
|||||||
# Seed Phrase Backup Reminder Implementation
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
This implementation adds a modal dialog that reminds users to back up their seed phrase if they haven't done so yet. The reminder appears after specific user actions and includes a 24-hour cooldown to avoid being too intrusive.
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
- **Modal Dialog**: Uses the existing notification group modal system from `App.vue`
|
|
||||||
- **Smart Timing**: Only shows when `hasBackedUpSeed = false`
|
|
||||||
- **24-Hour Cooldown**: Uses localStorage to prevent showing more than once per day
|
|
||||||
- **Action-Based Triggers**: Shows after specific user actions
|
|
||||||
- **User Choice**: "Backup Identifier Seed" or "Remind me Later" options
|
|
||||||
|
|
||||||
## Implementation Details
|
|
||||||
|
|
||||||
### Core Utility (`src/utils/seedPhraseReminder.ts`)
|
|
||||||
|
|
||||||
The main utility provides:
|
|
||||||
|
|
||||||
- `shouldShowSeedReminder(hasBackedUpSeed)`: Checks if reminder should be shown
|
|
||||||
- `markSeedReminderShown()`: Updates localStorage timestamp
|
|
||||||
- `createSeedReminderNotification()`: Creates the modal configuration
|
|
||||||
- `showSeedPhraseReminder(hasBackedUpSeed, notifyFunction)`: Main function to show reminder
|
|
||||||
|
|
||||||
### Trigger Points
|
|
||||||
|
|
||||||
The reminder is shown after these user actions:
|
|
||||||
|
|
||||||
**Note**: The reminder is triggered by **claim creation** actions, not claim confirmations. This focuses on when users are actively creating new content rather than just confirming existing claims.
|
|
||||||
|
|
||||||
1. **Profile Saving** (`AccountViewView.vue`)
|
|
||||||
- After clicking "Save Profile" button
|
|
||||||
- Only when profile save is successful
|
|
||||||
|
|
||||||
2. **Claim Creation** (Multiple views)
|
|
||||||
- `ClaimAddRawView.vue`: After submitting raw claims
|
|
||||||
- `GiftedDialog.vue`: After creating gifts/claims
|
|
||||||
- `GiftedDetailsView.vue`: After recording gifts/claims
|
|
||||||
- `OfferDialog.vue`: After creating offers
|
|
||||||
|
|
||||||
3. **QR Code Views Exit**
|
|
||||||
- `ContactQRScanFullView.vue`: When exiting via back button
|
|
||||||
- `ContactQRScanShowView.vue`: When exiting via back button
|
|
||||||
|
|
||||||
### Modal Configuration
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
{
|
|
||||||
group: "modal",
|
|
||||||
type: "confirm",
|
|
||||||
title: "Backup Your Identifier Seed?",
|
|
||||||
text: "It looks like you haven't backed up your identifier seed yet. It's important to back it up as soon as possible to secure your identity.",
|
|
||||||
yesText: "Backup Identifier Seed",
|
|
||||||
noText: "Remind me Later",
|
|
||||||
onYes: () => navigate to /seed-backup,
|
|
||||||
onNo: () => mark as shown for 24 hours,
|
|
||||||
onCancel: () => mark as shown for 24 hours
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Important**: The modal is configured with `timeout: -1` to ensure it stays open until the user explicitly interacts with one of the buttons. This prevents the dialog from closing automatically.
|
|
||||||
|
|
||||||
### Cooldown Mechanism
|
|
||||||
|
|
||||||
- **Storage Key**: `seedPhraseReminderLastShown`
|
|
||||||
- **Cooldown Period**: 24 hours (24 * 60 * 60 * 1000 milliseconds)
|
|
||||||
- **Implementation**: localStorage with timestamp comparison
|
|
||||||
- **Fallback**: Shows reminder if timestamp is invalid or missing
|
|
||||||
|
|
||||||
## User Experience
|
|
||||||
|
|
||||||
### When Reminder Appears
|
|
||||||
|
|
||||||
- User has not backed up their seed phrase (`hasBackedUpSeed = false`)
|
|
||||||
- At least 24 hours have passed since last reminder
|
|
||||||
- User performs one of the trigger actions
|
|
||||||
- **1-second delay** after the success message to allow users to see the confirmation
|
|
||||||
|
|
||||||
### User Options
|
|
||||||
|
|
||||||
1. **"Backup Identifier Seed"**: Navigates to `/seed-backup` page
|
|
||||||
2. **"Remind me Later"**: Dismisses and won't show again for 24 hours
|
|
||||||
3. **Cancel/Close**: Same behavior as "Remind me Later"
|
|
||||||
|
|
||||||
### Frequency Control
|
|
||||||
|
|
||||||
- **First Time**: Always shows if user hasn't backed up
|
|
||||||
- **Subsequent**: Only shows after 24-hour cooldown
|
|
||||||
- **Automatic Reset**: When user completes seed backup (`hasBackedUpSeed = true`)
|
|
||||||
|
|
||||||
## Technical Implementation
|
|
||||||
|
|
||||||
### Error Handling
|
|
||||||
|
|
||||||
- Graceful fallback if localStorage operations fail
|
|
||||||
- Logging of errors for debugging
|
|
||||||
- Non-blocking implementation (doesn't affect main functionality)
|
|
||||||
|
|
||||||
### Integration Points
|
|
||||||
|
|
||||||
- **Platform Service**: Uses `$accountSettings()` to check backup status
|
|
||||||
- **Notification System**: Integrates with existing `$notify` system
|
|
||||||
- **Router**: Uses `window.location.href` for navigation
|
|
||||||
|
|
||||||
### Performance Considerations
|
|
||||||
|
|
||||||
- Minimal localStorage operations
|
|
||||||
- No blocking operations
|
|
||||||
- Efficient timestamp comparisons
|
|
||||||
- **Timing Behavior**: 1-second delay before showing reminder to improve user experience flow
|
|
||||||
|
|
||||||
## Testing
|
|
||||||
|
|
||||||
### Manual Testing Scenarios
|
|
||||||
|
|
||||||
1. **First Time User**
|
|
||||||
- Create new account
|
|
||||||
- Perform trigger action (save profile, create claim, exit QR view)
|
|
||||||
- Verify reminder appears
|
|
||||||
|
|
||||||
2. **Repeat User (Within 24h)**
|
|
||||||
- Perform trigger action
|
|
||||||
- Verify reminder does NOT appear
|
|
||||||
|
|
||||||
3. **Repeat User (After 24h)**
|
|
||||||
- Wait 24+ hours
|
|
||||||
- Perform trigger action
|
|
||||||
- Verify reminder appears again
|
|
||||||
|
|
||||||
4. **User Who Has Backed Up**
|
|
||||||
- Complete seed backup
|
|
||||||
- Perform trigger action
|
|
||||||
- Verify reminder does NOT appear
|
|
||||||
|
|
||||||
5. **QR Code View Exit**
|
|
||||||
- Navigate to QR code view (full or show)
|
|
||||||
- Exit via back button
|
|
||||||
- Verify reminder appears (if conditions are met)
|
|
||||||
|
|
||||||
### Browser Testing
|
|
||||||
|
|
||||||
- Test localStorage functionality
|
|
||||||
- Verify timestamp handling
|
|
||||||
- Check navigation to seed backup page
|
|
||||||
|
|
||||||
## Future Enhancements
|
|
||||||
|
|
||||||
### Potential Improvements
|
|
||||||
|
|
||||||
1. **Customizable Cooldown**: Allow users to set reminder frequency
|
|
||||||
2. **Progressive Urgency**: Increase reminder frequency over time
|
|
||||||
3. **Analytics**: Track reminder effectiveness and user response
|
|
||||||
4. **A/B Testing**: Test different reminder messages and timing
|
|
||||||
|
|
||||||
### Configuration Options
|
|
||||||
|
|
||||||
- Reminder frequency settings
|
|
||||||
- Custom reminder messages
|
|
||||||
- Different trigger conditions
|
|
||||||
- Integration with other notification systems
|
|
||||||
|
|
||||||
## Maintenance
|
|
||||||
|
|
||||||
### Monitoring
|
|
||||||
|
|
||||||
- Check localStorage usage in browser dev tools
|
|
||||||
- Monitor user feedback about reminder frequency
|
|
||||||
- Track navigation success to seed backup page
|
|
||||||
|
|
||||||
### Updates
|
|
||||||
|
|
||||||
- Modify reminder text in `createSeedReminderNotification()`
|
|
||||||
- Adjust cooldown period in `REMINDER_COOLDOWN_MS` constant
|
|
||||||
- Add new trigger points as needed
|
|
||||||
|
|
||||||
## Conclusion
|
|
||||||
|
|
||||||
This implementation provides a non-intrusive way to remind users about seed phrase backup while respecting their preferences and avoiding notification fatigue. The 24-hour cooldown ensures users aren't overwhelmed while maintaining the importance of the security reminder.
|
|
||||||
|
|
||||||
The feature is fully integrated with the existing codebase architecture and follows established patterns for notifications, error handling, and user interaction.
|
|
||||||
@@ -1,116 +0,0 @@
|
|||||||
import { CapacitorConfig } from '@capacitor/cli';
|
|
||||||
|
|
||||||
const config: CapacitorConfig = {
|
|
||||||
appId: 'app.timesafari',
|
|
||||||
appName: 'TimeSafari',
|
|
||||||
webDir: 'dist',
|
|
||||||
server: {
|
|
||||||
cleartext: true
|
|
||||||
},
|
|
||||||
plugins: {
|
|
||||||
App: {
|
|
||||||
appUrlOpen: {
|
|
||||||
handlers: [
|
|
||||||
{
|
|
||||||
url: 'timesafari://*',
|
|
||||||
autoVerify: true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
SplashScreen: {
|
|
||||||
launchShowDuration: 3000,
|
|
||||||
launchAutoHide: true,
|
|
||||||
backgroundColor: '#ffffff',
|
|
||||||
androidSplashResourceName: 'splash',
|
|
||||||
androidScaleType: 'CENTER_CROP',
|
|
||||||
showSpinner: false,
|
|
||||||
androidSpinnerStyle: 'large',
|
|
||||||
iosSpinnerStyle: 'small',
|
|
||||||
spinnerColor: '#999999',
|
|
||||||
splashFullScreen: true,
|
|
||||||
splashImmersive: true
|
|
||||||
},
|
|
||||||
CapSQLite: {
|
|
||||||
iosDatabaseLocation: 'Library/CapacitorDatabase',
|
|
||||||
iosIsEncryption: false,
|
|
||||||
iosBiometric: {
|
|
||||||
biometricAuth: false,
|
|
||||||
biometricTitle: 'Biometric login for TimeSafari'
|
|
||||||
},
|
|
||||||
androidIsEncryption: false,
|
|
||||||
androidBiometric: {
|
|
||||||
biometricAuth: false,
|
|
||||||
biometricTitle: 'Biometric login for TimeSafari'
|
|
||||||
},
|
|
||||||
electronIsEncryption: false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
ios: {
|
|
||||||
contentInset: 'never',
|
|
||||||
allowsLinkPreview: true,
|
|
||||||
scrollEnabled: true,
|
|
||||||
limitsNavigationsToAppBoundDomains: true,
|
|
||||||
backgroundColor: '#ffffff',
|
|
||||||
allowNavigation: [
|
|
||||||
'*.timesafari.app',
|
|
||||||
'*.jsdelivr.net',
|
|
||||||
'api.endorser.ch'
|
|
||||||
]
|
|
||||||
},
|
|
||||||
android: {
|
|
||||||
allowMixedContent: true,
|
|
||||||
captureInput: true,
|
|
||||||
webContentsDebuggingEnabled: false,
|
|
||||||
allowNavigation: [
|
|
||||||
'*.timesafari.app',
|
|
||||||
'*.jsdelivr.net',
|
|
||||||
'api.endorser.ch',
|
|
||||||
'10.0.2.2:3000'
|
|
||||||
]
|
|
||||||
},
|
|
||||||
electron: {
|
|
||||||
deepLinking: {
|
|
||||||
schemes: ['timesafari']
|
|
||||||
},
|
|
||||||
buildOptions: {
|
|
||||||
appId: 'app.timesafari',
|
|
||||||
productName: 'TimeSafari',
|
|
||||||
directories: {
|
|
||||||
output: 'dist-electron-packages'
|
|
||||||
},
|
|
||||||
files: [
|
|
||||||
'dist/**/*',
|
|
||||||
'electron/**/*'
|
|
||||||
],
|
|
||||||
mac: {
|
|
||||||
category: 'public.app-category.productivity',
|
|
||||||
target: [
|
|
||||||
{
|
|
||||||
target: 'dmg',
|
|
||||||
arch: ['x64', 'arm64']
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
win: {
|
|
||||||
target: [
|
|
||||||
{
|
|
||||||
target: 'nsis',
|
|
||||||
arch: ['x64']
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
linux: {
|
|
||||||
target: [
|
|
||||||
{
|
|
||||||
target: 'AppImage',
|
|
||||||
arch: ['x64']
|
|
||||||
}
|
|
||||||
],
|
|
||||||
category: 'Utility'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default config;
|
|
||||||
1
electron/package-lock.json
generated
1
electron/package-lock.json
generated
@@ -56,6 +56,7 @@
|
|||||||
"version": "6.0.2",
|
"version": "6.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@capacitor-community/sqlite/-/sqlite-6.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@capacitor-community/sqlite/-/sqlite-6.0.2.tgz",
|
||||||
"integrity": "sha512-sj+2SPLu7E/3dM3xxcWwfNomG+aQHuN96/EFGrOtp4Dv30/2y5oIPyi6hZGjQGjPc5GDNoTQwW7vxWNzybjuMg==",
|
"integrity": "sha512-sj+2SPLu7E/3dM3xxcWwfNomG+aQHuN96/EFGrOtp4Dv30/2y5oIPyi6hZGjQGjPc5GDNoTQwW7vxWNzybjuMg==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"jeep-sqlite": "^2.7.2"
|
"jeep-sqlite": "^2.7.2"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -50,7 +50,6 @@ process.stderr.on('error', (err) => {
|
|||||||
const trayMenuTemplate: (MenuItemConstructorOptions | MenuItem)[] = [new MenuItem({ label: 'Quit App', role: 'quit' })];
|
const trayMenuTemplate: (MenuItemConstructorOptions | MenuItem)[] = [new MenuItem({ label: 'Quit App', role: 'quit' })];
|
||||||
const appMenuBarMenuTemplate: (MenuItemConstructorOptions | MenuItem)[] = [
|
const appMenuBarMenuTemplate: (MenuItemConstructorOptions | MenuItem)[] = [
|
||||||
{ role: process.platform === 'darwin' ? 'appMenu' : 'fileMenu' },
|
{ role: process.platform === 'darwin' ? 'appMenu' : 'fileMenu' },
|
||||||
{ role: 'editMenu' },
|
|
||||||
{ role: 'viewMenu' },
|
{ role: 'viewMenu' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -53,7 +53,6 @@ export class ElectronCapacitorApp {
|
|||||||
];
|
];
|
||||||
private AppMenuBarMenuTemplate: (MenuItem | MenuItemConstructorOptions)[] = [
|
private AppMenuBarMenuTemplate: (MenuItem | MenuItemConstructorOptions)[] = [
|
||||||
{ role: process.platform === 'darwin' ? 'appMenu' : 'fileMenu' },
|
{ role: process.platform === 'darwin' ? 'appMenu' : 'fileMenu' },
|
||||||
{ role: 'editMenu' },
|
|
||||||
{ role: 'viewMenu' },
|
{ role: 'viewMenu' },
|
||||||
];
|
];
|
||||||
private mainWindowState;
|
private mainWindowState;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"compileOnSave": true,
|
"compileOnSave": true,
|
||||||
"include": ["./src/**/*"],
|
"include": ["./src/**/*", "./capacitor.config.ts", "./capacitor.config.js"],
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"outDir": "./build",
|
"outDir": "./build",
|
||||||
"importHelpers": true,
|
"importHelpers": true,
|
||||||
|
|||||||
91
package-lock.json
generated
91
package-lock.json
generated
@@ -27,6 +27,7 @@
|
|||||||
"@ethersproject/hdnode": "^5.7.0",
|
"@ethersproject/hdnode": "^5.7.0",
|
||||||
"@ethersproject/wallet": "^5.8.0",
|
"@ethersproject/wallet": "^5.8.0",
|
||||||
"@fortawesome/fontawesome-svg-core": "^6.5.1",
|
"@fortawesome/fontawesome-svg-core": "^6.5.1",
|
||||||
|
"@fortawesome/free-regular-svg-icons": "^6.7.2",
|
||||||
"@fortawesome/free-solid-svg-icons": "^6.5.1",
|
"@fortawesome/free-solid-svg-icons": "^6.5.1",
|
||||||
"@fortawesome/vue-fontawesome": "^3.0.6",
|
"@fortawesome/vue-fontawesome": "^3.0.6",
|
||||||
"@jlongster/sql.js": "^1.6.7",
|
"@jlongster/sql.js": "^1.6.7",
|
||||||
@@ -90,6 +91,7 @@
|
|||||||
"vue": "3.5.13",
|
"vue": "3.5.13",
|
||||||
"vue-axios": "^3.5.2",
|
"vue-axios": "^3.5.2",
|
||||||
"vue-facing-decorator": "3.0.4",
|
"vue-facing-decorator": "3.0.4",
|
||||||
|
"vue-markdown-render": "^2.2.1",
|
||||||
"vue-picture-cropper": "^0.7.0",
|
"vue-picture-cropper": "^0.7.0",
|
||||||
"vue-qrcode-reader": "^5.5.3",
|
"vue-qrcode-reader": "^5.5.3",
|
||||||
"vue-router": "^4.5.0",
|
"vue-router": "^4.5.0",
|
||||||
@@ -106,6 +108,7 @@
|
|||||||
"@types/js-yaml": "^4.0.9",
|
"@types/js-yaml": "^4.0.9",
|
||||||
"@types/leaflet": "^1.9.8",
|
"@types/leaflet": "^1.9.8",
|
||||||
"@types/luxon": "^3.4.2",
|
"@types/luxon": "^3.4.2",
|
||||||
|
"@types/markdown-it": "^14.1.2",
|
||||||
"@types/node": "^20.14.11",
|
"@types/node": "^20.14.11",
|
||||||
"@types/node-fetch": "^2.6.12",
|
"@types/node-fetch": "^2.6.12",
|
||||||
"@types/ramda": "^0.29.11",
|
"@types/ramda": "^0.29.11",
|
||||||
@@ -6786,6 +6789,17 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@fortawesome/free-regular-svg-icons": {
|
||||||
|
"version": "6.7.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.7.2.tgz",
|
||||||
|
"integrity": "sha512-7Z/ur0gvCMW8G93dXIQOkQqHo2M5HLhYrRVC0//fakJXxcF1VmMPsxnG6Ee8qEylA8b8Q3peQXWMNZ62lYF28g==",
|
||||||
|
"dependencies": {
|
||||||
|
"@fortawesome/fontawesome-common-types": "6.7.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@fortawesome/free-solid-svg-icons": {
|
"node_modules/@fortawesome/free-solid-svg-icons": {
|
||||||
"version": "6.7.2",
|
"version": "6.7.2",
|
||||||
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.7.2.tgz",
|
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.7.2.tgz",
|
||||||
@@ -10147,6 +10161,12 @@
|
|||||||
"@types/geojson": "*"
|
"@types/geojson": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/linkify-it": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/@types/luxon": {
|
"node_modules/@types/luxon": {
|
||||||
"version": "3.7.1",
|
"version": "3.7.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.7.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.7.1.tgz",
|
||||||
@@ -10154,6 +10174,22 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/markdown-it": {
|
||||||
|
"version": "14.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz",
|
||||||
|
"integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@types/linkify-it": "^5",
|
||||||
|
"@types/mdurl": "^2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/mdurl": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/@types/minimist": {
|
"node_modules/@types/minimist": {
|
||||||
"version": "1.2.5",
|
"version": "1.2.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.5.tgz",
|
||||||
@@ -32883,6 +32919,61 @@
|
|||||||
"vue": "^3.0.0"
|
"vue": "^3.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/vue-markdown-render": {
|
||||||
|
"version": "2.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/vue-markdown-render/-/vue-markdown-render-2.2.1.tgz",
|
||||||
|
"integrity": "sha512-XkYnC0PMdbs6Vy6j/gZXSvCuOS0787Se5COwXlepRqiqPiunyCIeTPQAO2XnB4Yl04EOHXwLx5y6IuszMWSgyQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"markdown-it": "^13.0.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"vue": "^3.3.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/vue-markdown-render/node_modules/entities": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/entities/-/entities-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/vue-markdown-render/node_modules/linkify-it": {
|
||||||
|
"version": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-4.0.1.tgz",
|
||||||
|
"integrity": "sha512-C7bfi1UZmoj8+PQx22XyeXCuBlokoyWQL5pWSP+EI6nzRylyThouddufc2c1NDIcP9k5agmN9fLpA7VNJfIiqw==",
|
||||||
|
"dependencies": {
|
||||||
|
"uc.micro": "^1.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/vue-markdown-render/node_modules/markdown-it": {
|
||||||
|
"version": "13.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-13.0.2.tgz",
|
||||||
|
"integrity": "sha512-FtwnEuuK+2yVU7goGn/MJ0WBZMM9ZPgU9spqlFs7/A/pDIUNSOQZhUgOqYCficIuR2QaFnrt8LHqBWsbTAoI5w==",
|
||||||
|
"dependencies": {
|
||||||
|
"argparse": "^2.0.1",
|
||||||
|
"entities": "~3.0.1",
|
||||||
|
"linkify-it": "^4.0.1",
|
||||||
|
"mdurl": "^1.0.1",
|
||||||
|
"uc.micro": "^1.0.5"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"markdown-it": "bin/markdown-it.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/vue-markdown-render/node_modules/mdurl": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g=="
|
||||||
|
},
|
||||||
|
"node_modules/vue-markdown-render/node_modules/uc.micro": {
|
||||||
|
"version": "1.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz",
|
||||||
|
"integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA=="
|
||||||
|
},
|
||||||
"node_modules/vue-picture-cropper": {
|
"node_modules/vue-picture-cropper": {
|
||||||
"version": "0.7.0",
|
"version": "0.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/vue-picture-cropper/-/vue-picture-cropper-0.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/vue-picture-cropper/-/vue-picture-cropper-0.7.0.tgz",
|
||||||
|
|||||||
@@ -136,7 +136,6 @@
|
|||||||
"*.{js,ts,vue,css,json,yml,yaml}": "eslint --fix || true",
|
"*.{js,ts,vue,css,json,yml,yaml}": "eslint --fix || true",
|
||||||
"*.{md,markdown,mdc}": "markdownlint-cli2 --fix"
|
"*.{md,markdown,mdc}": "markdownlint-cli2 --fix"
|
||||||
},
|
},
|
||||||
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@capacitor-community/electron": "^5.0.1",
|
"@capacitor-community/electron": "^5.0.1",
|
||||||
"@capacitor-community/sqlite": "6.0.2",
|
"@capacitor-community/sqlite": "6.0.2",
|
||||||
@@ -157,6 +156,7 @@
|
|||||||
"@ethersproject/hdnode": "^5.7.0",
|
"@ethersproject/hdnode": "^5.7.0",
|
||||||
"@ethersproject/wallet": "^5.8.0",
|
"@ethersproject/wallet": "^5.8.0",
|
||||||
"@fortawesome/fontawesome-svg-core": "^6.5.1",
|
"@fortawesome/fontawesome-svg-core": "^6.5.1",
|
||||||
|
"@fortawesome/free-regular-svg-icons": "^6.7.2",
|
||||||
"@fortawesome/free-solid-svg-icons": "^6.5.1",
|
"@fortawesome/free-solid-svg-icons": "^6.5.1",
|
||||||
"@fortawesome/vue-fontawesome": "^3.0.6",
|
"@fortawesome/vue-fontawesome": "^3.0.6",
|
||||||
"@jlongster/sql.js": "^1.6.7",
|
"@jlongster/sql.js": "^1.6.7",
|
||||||
@@ -220,6 +220,7 @@
|
|||||||
"vue": "3.5.13",
|
"vue": "3.5.13",
|
||||||
"vue-axios": "^3.5.2",
|
"vue-axios": "^3.5.2",
|
||||||
"vue-facing-decorator": "3.0.4",
|
"vue-facing-decorator": "3.0.4",
|
||||||
|
"vue-markdown-render": "^2.2.1",
|
||||||
"vue-picture-cropper": "^0.7.0",
|
"vue-picture-cropper": "^0.7.0",
|
||||||
"vue-qrcode-reader": "^5.5.3",
|
"vue-qrcode-reader": "^5.5.3",
|
||||||
"vue-router": "^4.5.0",
|
"vue-router": "^4.5.0",
|
||||||
@@ -236,6 +237,7 @@
|
|||||||
"@types/js-yaml": "^4.0.9",
|
"@types/js-yaml": "^4.0.9",
|
||||||
"@types/leaflet": "^1.9.8",
|
"@types/leaflet": "^1.9.8",
|
||||||
"@types/luxon": "^3.4.2",
|
"@types/luxon": "^3.4.2",
|
||||||
|
"@types/markdown-it": "^14.1.2",
|
||||||
"@types/node": "^20.14.11",
|
"@types/node": "^20.14.11",
|
||||||
"@types/node-fetch": "^2.6.12",
|
"@types/node-fetch": "^2.6.12",
|
||||||
"@types/ramda": "^0.29.11",
|
"@types/ramda": "^0.29.11",
|
||||||
|
|||||||
@@ -181,7 +181,7 @@ sync_capacitor() {
|
|||||||
copy_web_assets() {
|
copy_web_assets() {
|
||||||
log_info "Copying web assets to Electron"
|
log_info "Copying web assets to Electron"
|
||||||
safe_execute "Copying assets" "cp -r dist/* electron/app/"
|
safe_execute "Copying assets" "cp -r dist/* electron/app/"
|
||||||
# Note: Electron has its own capacitor.config.ts file, so we don't copy the main config
|
safe_execute "Copying config" "cp capacitor.config.json electron/capacitor.config.json"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Compile TypeScript
|
# Compile TypeScript
|
||||||
|
|||||||
@@ -22,4 +22,24 @@
|
|||||||
.dialog {
|
.dialog {
|
||||||
@apply bg-white p-4 rounded-lg w-full max-w-lg;
|
@apply bg-white p-4 rounded-lg w-full max-w-lg;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Markdown content styling to restore list elements */
|
||||||
|
.markdown-content ul {
|
||||||
|
@apply list-disc list-inside ml-4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content ol {
|
||||||
|
@apply list-decimal list-inside ml-4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content li {
|
||||||
|
@apply mb-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content ul ul,
|
||||||
|
.markdown-content ol ol,
|
||||||
|
.markdown-content ul ol,
|
||||||
|
.markdown-content ol ul {
|
||||||
|
@apply ml-4 mt-1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -80,7 +80,10 @@
|
|||||||
<!-- Description -->
|
<!-- Description -->
|
||||||
<p class="font-medium">
|
<p class="font-medium">
|
||||||
<a class="cursor-pointer" @click="emitLoadClaim(record.jwtId)">
|
<a class="cursor-pointer" @click="emitLoadClaim(record.jwtId)">
|
||||||
{{ description }}
|
<vue-markdown
|
||||||
|
:source="truncatedDescription"
|
||||||
|
class="markdown-content"
|
||||||
|
/>
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@@ -258,11 +261,13 @@ import {
|
|||||||
NOTIFY_UNKNOWN_PERSON,
|
NOTIFY_UNKNOWN_PERSON,
|
||||||
} from "@/constants/notifications";
|
} from "@/constants/notifications";
|
||||||
import { TIMEOUTS } from "@/utils/notify";
|
import { TIMEOUTS } from "@/utils/notify";
|
||||||
|
import VueMarkdown from "vue-markdown-render";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: {
|
components: {
|
||||||
EntityIcon,
|
EntityIcon,
|
||||||
ProjectIcon,
|
ProjectIcon,
|
||||||
|
VueMarkdown,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
export default class ActivityListItem extends Vue {
|
export default class ActivityListItem extends Vue {
|
||||||
@@ -303,6 +308,14 @@ export default class ActivityListItem extends Vue {
|
|||||||
return `${claim?.description || ""}`;
|
return `${claim?.description || ""}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get truncatedDescription(): string {
|
||||||
|
const desc = this.description;
|
||||||
|
if (desc.length <= 300) {
|
||||||
|
return desc;
|
||||||
|
}
|
||||||
|
return desc.substring(0, 300) + "...";
|
||||||
|
}
|
||||||
|
|
||||||
private displayAmount(code: string, amt: number) {
|
private displayAmount(code: string, amt: number) {
|
||||||
return `${amt} ${this.currencyShortWordForCode(code, amt === 1)}`;
|
return `${amt} ${this.currencyShortWordForCode(code, amt === 1)}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,12 +16,6 @@ messages * - Conditional UI based on platform capabilities * * @component *
|
|||||||
:to="{ name: 'seed-backup' }"
|
:to="{ name: 'seed-backup' }"
|
||||||
:class="backupButtonClasses"
|
:class="backupButtonClasses"
|
||||||
>
|
>
|
||||||
<!-- Notification dot - show while the user has not yet backed up their seed phrase -->
|
|
||||||
<font-awesome
|
|
||||||
v-if="!hasBackedUpSeed"
|
|
||||||
icon="circle"
|
|
||||||
class="absolute -right-[8px] -top-[8px] text-rose-500 text-[14px] border border-white rounded-full"
|
|
||||||
></font-awesome>
|
|
||||||
Backup Identifier Seed
|
Backup Identifier Seed
|
||||||
</router-link>
|
</router-link>
|
||||||
|
|
||||||
@@ -104,12 +98,6 @@ export default class DataExportSection extends Vue {
|
|||||||
*/
|
*/
|
||||||
isExporting = false;
|
isExporting = false;
|
||||||
|
|
||||||
/**
|
|
||||||
* Flag indicating if the user has backed up their seed phrase
|
|
||||||
* Used to control the visibility of the notification dot
|
|
||||||
*/
|
|
||||||
hasBackedUpSeed = false;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Notification helper for consistent notification patterns
|
* Notification helper for consistent notification patterns
|
||||||
* Created as a getter to ensure $notify is available when called
|
* Created as a getter to ensure $notify is available when called
|
||||||
@@ -141,7 +129,7 @@ export default class DataExportSection extends Vue {
|
|||||||
* CSS classes for the backup button (router link)
|
* CSS classes for the backup button (router link)
|
||||||
*/
|
*/
|
||||||
get backupButtonClasses(): string {
|
get backupButtonClasses(): string {
|
||||||
return "block relative 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";
|
return "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";
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -230,22 +218,6 @@ export default class DataExportSection extends Vue {
|
|||||||
|
|
||||||
created() {
|
created() {
|
||||||
this.notify = createNotifyHelpers(this.$notify);
|
this.notify = createNotifyHelpers(this.$notify);
|
||||||
this.loadSeedBackupStatus();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Loads the seed backup status from account settings
|
|
||||||
* Updates the hasBackedUpSeed flag to control notification dot visibility
|
|
||||||
*/
|
|
||||||
private async loadSeedBackupStatus(): Promise<void> {
|
|
||||||
try {
|
|
||||||
const settings = await this.$accountSettings();
|
|
||||||
this.hasBackedUpSeed = !!settings.hasBackedUpSeed;
|
|
||||||
} catch (err: unknown) {
|
|
||||||
logger.error("Failed to load seed backup status:", err);
|
|
||||||
// Default to false (show notification dot) if we can't load the setting
|
|
||||||
this.hasBackedUpSeed = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -82,7 +82,6 @@ import GiftDetailsStep from "../components/GiftDetailsStep.vue";
|
|||||||
import { PlanData } from "../interfaces/records";
|
import { PlanData } from "../interfaces/records";
|
||||||
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
||||||
import { createNotifyHelpers, TIMEOUTS, NotifyFunction } from "@/utils/notify";
|
import { createNotifyHelpers, TIMEOUTS, NotifyFunction } from "@/utils/notify";
|
||||||
import { showSeedPhraseReminder } from "@/utils/seedPhraseReminder";
|
|
||||||
import {
|
import {
|
||||||
NOTIFY_GIFT_ERROR_NEGATIVE_AMOUNT,
|
NOTIFY_GIFT_ERROR_NEGATIVE_AMOUNT,
|
||||||
NOTIFY_GIFT_ERROR_NO_DESCRIPTION,
|
NOTIFY_GIFT_ERROR_NO_DESCRIPTION,
|
||||||
@@ -412,15 +411,6 @@ export default class GiftedDialog extends Vue {
|
|||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
this.safeNotify.success("That gift was recorded.", TIMEOUTS.VERY_LONG);
|
this.safeNotify.success("That gift was recorded.", TIMEOUTS.VERY_LONG);
|
||||||
|
|
||||||
// Show seed phrase backup reminder if needed
|
|
||||||
try {
|
|
||||||
const settings = await this.$accountSettings();
|
|
||||||
showSeedPhraseReminder(!!settings.hasBackedUpSeed, this.$notify);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error("Error checking seed backup status:", error);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.callbackOnSuccess) {
|
if (this.callbackOnSuccess) {
|
||||||
this.callbackOnSuccess(amount);
|
this.callbackOnSuccess(amount);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,7 +74,7 @@
|
|||||||
If you'd like an introduction,
|
If you'd like an introduction,
|
||||||
<a
|
<a
|
||||||
class="text-blue-500"
|
class="text-blue-500"
|
||||||
@click="copyTextToClipboard('A link to this page', deepLinkUrl)"
|
@click="copyToClipboard('A link to this page', deepLinkUrl)"
|
||||||
>click here to copy this page, paste it into a message, and ask if
|
>click here to copy this page, paste it into a message, and ask if
|
||||||
they'll tell you more about the {{ roleName }}.</a
|
they'll tell you more about the {{ roleName }}.</a
|
||||||
>
|
>
|
||||||
@@ -110,7 +110,7 @@
|
|||||||
* @since 2024-12-19
|
* @since 2024-12-19
|
||||||
*/
|
*/
|
||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
import { copyToClipboard } from "../services/ClipboardService";
|
import { useClipboard } from "@vueuse/core";
|
||||||
import * as R from "ramda";
|
import * as R from "ramda";
|
||||||
import * as serverUtil from "../libs/endorserServer";
|
import * as serverUtil from "../libs/endorserServer";
|
||||||
import { Contact } from "../db/tables/contacts";
|
import { Contact } from "../db/tables/contacts";
|
||||||
@@ -197,24 +197,19 @@ export default class HiddenDidDialog extends Vue {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async copyTextToClipboard(name: string, text: string) {
|
copyToClipboard(name: string, text: string) {
|
||||||
try {
|
useClipboard()
|
||||||
await copyToClipboard(text);
|
.copy(text)
|
||||||
this.notify.success(
|
.then(() => {
|
||||||
NOTIFY_COPIED_TO_CLIPBOARD.message(name || "That"),
|
this.notify.success(
|
||||||
TIMEOUTS.SHORT,
|
NOTIFY_COPIED_TO_CLIPBOARD.message(name || "That"),
|
||||||
);
|
TIMEOUTS.SHORT,
|
||||||
} catch (error) {
|
);
|
||||||
this.$logAndConsole(
|
});
|
||||||
`Error copying ${name || "content"} to clipboard: ${error}`,
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
this.notify.error(`Failed to copy ${name || "content"} to clipboard.`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onClickShareClaim() {
|
onClickShareClaim() {
|
||||||
this.copyTextToClipboard("A link to this page", this.deepLinkUrl);
|
this.copyToClipboard("A link to this page", this.deepLinkUrl);
|
||||||
window.navigator.share({
|
window.navigator.share({
|
||||||
title: "Help Connect Me",
|
title: "Help Connect Me",
|
||||||
text: "I'm trying to find the people who recorded this. Can you help me?",
|
text: "I'm trying to find the people who recorded this. Can you help me?",
|
||||||
|
|||||||
@@ -64,7 +64,6 @@ import * as libsUtil from "../libs/util";
|
|||||||
import { logger } from "../utils/logger";
|
import { logger } from "../utils/logger";
|
||||||
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
||||||
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
|
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
|
||||||
import { showSeedPhraseReminder } from "@/utils/seedPhraseReminder";
|
|
||||||
import {
|
import {
|
||||||
NOTIFY_OFFER_SETTINGS_ERROR,
|
NOTIFY_OFFER_SETTINGS_ERROR,
|
||||||
NOTIFY_OFFER_RECORDING,
|
NOTIFY_OFFER_RECORDING,
|
||||||
@@ -300,14 +299,6 @@ export default class OfferDialog extends Vue {
|
|||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
this.notify.success(NOTIFY_OFFER_SUCCESS.message, TIMEOUTS.VERY_LONG);
|
this.notify.success(NOTIFY_OFFER_SUCCESS.message, TIMEOUTS.VERY_LONG);
|
||||||
|
|
||||||
// Show seed phrase backup reminder if needed
|
|
||||||
try {
|
|
||||||
const settings = await this.$accountSettings();
|
|
||||||
showSeedPhraseReminder(!!settings.hasBackedUpSeed, this.$notify);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error("Error checking seed backup status:", error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
|||||||
@@ -84,8 +84,7 @@ export default class UserNameDialog extends Vue {
|
|||||||
*/
|
*/
|
||||||
async open(aCallback?: (name?: string) => void) {
|
async open(aCallback?: (name?: string) => void) {
|
||||||
this.callback = aCallback || this.callback;
|
this.callback = aCallback || this.callback;
|
||||||
// Load from account-specific settings instead of master settings
|
const settings = await this.$settings();
|
||||||
const settings = await this.$accountSettings();
|
|
||||||
this.givenName = settings.firstName || "";
|
this.givenName = settings.firstName || "";
|
||||||
this.visible = true;
|
this.visible = true;
|
||||||
}
|
}
|
||||||
@@ -96,18 +95,7 @@ export default class UserNameDialog extends Vue {
|
|||||||
*/
|
*/
|
||||||
async onClickSaveChanges() {
|
async onClickSaveChanges() {
|
||||||
try {
|
try {
|
||||||
// Get the current active DID to save to user-specific settings
|
await this.$updateSettings({ firstName: this.givenName });
|
||||||
const settings = await this.$accountSettings();
|
|
||||||
const activeDid = settings.activeDid;
|
|
||||||
|
|
||||||
if (activeDid) {
|
|
||||||
// Save to user-specific settings for the current identity
|
|
||||||
await this.$saveUserSettings(activeDid, { firstName: this.givenName });
|
|
||||||
} else {
|
|
||||||
// Fallback to master settings if no active DID
|
|
||||||
await this.$saveSettings({ firstName: this.givenName });
|
|
||||||
}
|
|
||||||
|
|
||||||
this.visible = false;
|
this.visible = false;
|
||||||
this.callback(this.givenName);
|
this.callback(this.givenName);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -1689,11 +1689,3 @@ export const NOTIFY_CONTACTS_ADDED_CONFIRM = {
|
|||||||
title: "They're Added To Your List",
|
title: "They're Added To Your List",
|
||||||
message: "Would you like to go to the main page now?",
|
message: "Would you like to go to the main page now?",
|
||||||
};
|
};
|
||||||
|
|
||||||
// ImportAccountView.vue specific constants
|
|
||||||
// Used in: ImportAccountView.vue (onImportClick method - duplicate account warning)
|
|
||||||
export const NOTIFY_DUPLICATE_ACCOUNT_IMPORT = {
|
|
||||||
title: "Account Already Imported",
|
|
||||||
message:
|
|
||||||
"This account has already been imported. Please use a different seed phrase or check your existing accounts.",
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -125,9 +125,10 @@ const MIGRATIONS = [
|
|||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "003_add_hasBackedUpSeed_to_settings",
|
name: "005_add_starredPlanHandleIds_to_settings",
|
||||||
sql: `
|
sql: `
|
||||||
ALTER TABLE settings ADD COLUMN hasBackedUpSeed BOOLEAN DEFAULT FALSE;
|
ALTER TABLE settings ADD COLUMN starredPlanHandleIds TEXT DEFAULT '[]'; -- JSON string
|
||||||
|
ALTER TABLE settings ADD COLUMN lastAckedStarredPlanChangesJwtId TEXT;
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -157,10 +157,11 @@ export async function retrieveSettingsForDefaultAccount(): Promise<Settings> {
|
|||||||
result.columns,
|
result.columns,
|
||||||
result.values,
|
result.values,
|
||||||
)[0] as Settings;
|
)[0] as Settings;
|
||||||
if (settings.searchBoxes) {
|
settings.searchBoxes = parseJsonField(settings.searchBoxes, []);
|
||||||
// @ts-expect-error - the searchBoxes field is a string in the DB
|
settings.starredPlanHandleIds = parseJsonField(
|
||||||
settings.searchBoxes = JSON.parse(settings.searchBoxes);
|
settings.starredPlanHandleIds,
|
||||||
}
|
[],
|
||||||
|
);
|
||||||
return settings;
|
return settings;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -226,10 +227,11 @@ export async function retrieveSettingsForActiveAccount(): Promise<Settings> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle searchBoxes parsing
|
settings.searchBoxes = parseJsonField(settings.searchBoxes, []);
|
||||||
if (settings.searchBoxes) {
|
settings.starredPlanHandleIds = parseJsonField(
|
||||||
settings.searchBoxes = parseJsonField(settings.searchBoxes, []);
|
settings.starredPlanHandleIds,
|
||||||
}
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
return settings;
|
return settings;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ export type Settings = {
|
|||||||
finishedOnboarding?: boolean; // the user has completed the onboarding process
|
finishedOnboarding?: boolean; // the user has completed the onboarding process
|
||||||
|
|
||||||
firstName?: string; // user's full name, may be null if unwanted for a particular account
|
firstName?: string; // user's full name, may be null if unwanted for a particular account
|
||||||
hasBackedUpSeed?: boolean; // tracks whether the user has backed up their seed phrase
|
|
||||||
hideRegisterPromptOnNewContact?: boolean;
|
hideRegisterPromptOnNewContact?: boolean;
|
||||||
isRegistered?: boolean;
|
isRegistered?: boolean;
|
||||||
// imageServer?: string; // if we want to allow modification then we should make image functionality optional -- or at least customizable
|
// imageServer?: string; // if we want to allow modification then we should make image functionality optional -- or at least customizable
|
||||||
@@ -37,6 +36,7 @@ export type Settings = {
|
|||||||
|
|
||||||
lastAckedOfferToUserJwtId?: string; // the last JWT ID for offer-to-user that they've acknowledged seeing
|
lastAckedOfferToUserJwtId?: string; // the last JWT ID for offer-to-user that they've acknowledged seeing
|
||||||
lastAckedOfferToUserProjectsJwtId?: string; // the last JWT ID for offers-to-user's-projects that they've acknowledged seeing
|
lastAckedOfferToUserProjectsJwtId?: string; // the last JWT ID for offers-to-user's-projects that they've acknowledged seeing
|
||||||
|
lastAckedStarredPlanChangesJwtId?: string; // the last JWT ID for starred plan changes that they've acknowledged seeing
|
||||||
|
|
||||||
// The claim list has a most recent one used in notifications that's separate from the last viewed
|
// The claim list has a most recent one used in notifications that's separate from the last viewed
|
||||||
lastNotifiedClaimId?: string;
|
lastNotifiedClaimId?: string;
|
||||||
@@ -61,15 +61,18 @@ export type Settings = {
|
|||||||
showContactGivesInline?: boolean; // Display contact inline or not
|
showContactGivesInline?: boolean; // Display contact inline or not
|
||||||
showGeneralAdvanced?: boolean; // Show advanced features which don't have their own flag
|
showGeneralAdvanced?: boolean; // Show advanced features which don't have their own flag
|
||||||
showShortcutBvc?: boolean; // Show shortcut for Bountiful Voluntaryist Community actions
|
showShortcutBvc?: boolean; // Show shortcut for Bountiful Voluntaryist Community actions
|
||||||
|
|
||||||
|
starredPlanHandleIds?: string[]; // Array of starred plan handle IDs
|
||||||
vapid?: string; // VAPID (Voluntary Application Server Identification) field for web push
|
vapid?: string; // VAPID (Voluntary Application Server Identification) field for web push
|
||||||
warnIfProdServer?: boolean; // Warn if using a production server
|
warnIfProdServer?: boolean; // Warn if using a production server
|
||||||
warnIfTestServer?: boolean; // Warn if using a testing server
|
warnIfTestServer?: boolean; // Warn if using a testing server
|
||||||
webPushServer?: string; // Web Push server URL
|
webPushServer?: string; // Web Push server URL
|
||||||
};
|
};
|
||||||
|
|
||||||
// type of settings where the searchBoxes are JSON strings instead of objects
|
// type of settings where the values are JSON strings instead of objects
|
||||||
export type SettingsWithJsonStrings = Settings & {
|
export type SettingsWithJsonStrings = Settings & {
|
||||||
searchBoxes: string;
|
searchBoxes: string;
|
||||||
|
starredPlanHandleIds: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function checkIsAnyFeedFilterOn(settings: Settings): boolean {
|
export function checkIsAnyFeedFilterOn(settings: Settings): boolean {
|
||||||
@@ -86,6 +89,11 @@ export const SettingsSchema = {
|
|||||||
/**
|
/**
|
||||||
* Constants.
|
* Constants.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is deprecated.
|
||||||
|
* It only remains for those with a PWA who have not migrated, but we'll soon remove it.
|
||||||
|
*/
|
||||||
export const MASTER_SETTINGS_KEY = "1";
|
export const MASTER_SETTINGS_KEY = "1";
|
||||||
|
|
||||||
export const DEFAULT_PASSKEY_EXPIRATION_MINUTES = 15;
|
export const DEFAULT_PASSKEY_EXPIRATION_MINUTES = 15;
|
||||||
|
|||||||
@@ -72,11 +72,15 @@ export interface PlanActionClaim extends ClaimObject {
|
|||||||
name: string;
|
name: string;
|
||||||
agent?: { identifier: string };
|
agent?: { identifier: string };
|
||||||
description?: string;
|
description?: string;
|
||||||
|
endTime?: string;
|
||||||
identifier?: string;
|
identifier?: string;
|
||||||
|
image?: string;
|
||||||
lastClaimId?: string;
|
lastClaimId?: string;
|
||||||
location?: {
|
location?: {
|
||||||
geo: { "@type": "GeoCoordinates"; latitude: number; longitude: number };
|
geo: { "@type": "GeoCoordinates"; latitude: number; longitude: number };
|
||||||
};
|
};
|
||||||
|
startTime?: string;
|
||||||
|
url?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// AKA Registration & RegisterAction
|
// AKA Registration & RegisterAction
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { GiveActionClaim, OfferClaim } from "./claims";
|
import { GiveActionClaim, OfferClaim, PlanActionClaim } from "./claims";
|
||||||
|
import { GenericCredWrapper } from "./common";
|
||||||
|
|
||||||
// a summary record; the VC is found the fullClaim field
|
// a summary record; the VC is found the fullClaim field
|
||||||
export interface GiveSummaryRecord {
|
export interface GiveSummaryRecord {
|
||||||
@@ -61,6 +62,11 @@ export interface PlanSummaryRecord {
|
|||||||
jwtId?: string;
|
jwtId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PlanSummaryAndPreviousClaim {
|
||||||
|
plan: PlanSummaryRecord;
|
||||||
|
wrappedClaimBefore: GenericCredWrapper<PlanActionClaim>;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents data about a project
|
* Represents data about a project
|
||||||
*
|
*
|
||||||
@@ -87,7 +93,10 @@ export interface PlanData {
|
|||||||
name: string;
|
name: string;
|
||||||
/**
|
/**
|
||||||
* The identifier of the project record -- different from jwtId
|
* The identifier of the project record -- different from jwtId
|
||||||
* (Maybe we should use the jwtId to iterate through the records instead.)
|
*
|
||||||
|
* This has been used to iterate through plan records, because jwtId ordering doesn't match
|
||||||
|
* chronological create ordering, though it does match most recent edit order (in reverse order).
|
||||||
|
* (It may be worthwhile to order by jwtId instead. It is an indexed field.)
|
||||||
**/
|
**/
|
||||||
rowId?: string;
|
rowId?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,7 +56,12 @@ import {
|
|||||||
KeyMetaWithPrivate,
|
KeyMetaWithPrivate,
|
||||||
KeyMetaMaybeWithPrivate,
|
KeyMetaMaybeWithPrivate,
|
||||||
} from "../interfaces/common";
|
} from "../interfaces/common";
|
||||||
import { PlanSummaryRecord } from "../interfaces/records";
|
import {
|
||||||
|
OfferSummaryRecord,
|
||||||
|
OfferToPlanSummaryRecord,
|
||||||
|
PlanSummaryAndPreviousClaim,
|
||||||
|
PlanSummaryRecord,
|
||||||
|
} from "../interfaces/records";
|
||||||
import { logger } from "../utils/logger";
|
import { logger } from "../utils/logger";
|
||||||
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
||||||
import { APP_SERVER } from "@/constants/app";
|
import { APP_SERVER } from "@/constants/app";
|
||||||
@@ -362,6 +367,22 @@ export function didInfo(
|
|||||||
return didInfoForContact(did, activeDid, contact, allMyDids).displayName;
|
return didInfoForContact(did, activeDid, contact, allMyDids).displayName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* In some contexts (eg. agent), a blank really is nobody.
|
||||||
|
*/
|
||||||
|
export function didInfoOrNobody(
|
||||||
|
did: string | undefined,
|
||||||
|
activeDid: string | undefined,
|
||||||
|
allMyDids: string[],
|
||||||
|
contacts: Contact[],
|
||||||
|
): string {
|
||||||
|
if (did == null) {
|
||||||
|
return "Nobody";
|
||||||
|
} else {
|
||||||
|
return didInfo(did, activeDid, allMyDids, contacts);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* return text description without any references to "you" as user
|
* return text description without any references to "you" as user
|
||||||
*/
|
*/
|
||||||
@@ -730,7 +751,7 @@ export async function getNewOffersToUser(
|
|||||||
activeDid: string,
|
activeDid: string,
|
||||||
afterOfferJwtId?: string,
|
afterOfferJwtId?: string,
|
||||||
beforeOfferJwtId?: string,
|
beforeOfferJwtId?: string,
|
||||||
) {
|
): Promise<{ data: Array<OfferSummaryRecord>; hitLimit: boolean }> {
|
||||||
let url = `${apiServer}/api/v2/report/offers?recipientDid=${activeDid}`;
|
let url = `${apiServer}/api/v2/report/offers?recipientDid=${activeDid}`;
|
||||||
if (afterOfferJwtId) {
|
if (afterOfferJwtId) {
|
||||||
url += "&afterId=" + afterOfferJwtId;
|
url += "&afterId=" + afterOfferJwtId;
|
||||||
@@ -752,7 +773,7 @@ export async function getNewOffersToUserProjects(
|
|||||||
activeDid: string,
|
activeDid: string,
|
||||||
afterOfferJwtId?: string,
|
afterOfferJwtId?: string,
|
||||||
beforeOfferJwtId?: string,
|
beforeOfferJwtId?: string,
|
||||||
) {
|
): Promise<{ data: Array<OfferToPlanSummaryRecord>; hitLimit: boolean }> {
|
||||||
let url = `${apiServer}/api/v2/report/offersToPlansOwnedByMe`;
|
let url = `${apiServer}/api/v2/report/offersToPlansOwnedByMe`;
|
||||||
if (afterOfferJwtId) {
|
if (afterOfferJwtId) {
|
||||||
url += "?afterId=" + afterOfferJwtId;
|
url += "?afterId=" + afterOfferJwtId;
|
||||||
@@ -766,6 +787,44 @@ export async function getNewOffersToUserProjects(
|
|||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get starred projects that have been updated since the last check
|
||||||
|
*
|
||||||
|
* @param axios - axios instance
|
||||||
|
* @param apiServer - endorser API server URL
|
||||||
|
* @param activeDid - user's DID for authentication
|
||||||
|
* @param starredPlanHandleIds - array of starred project handle IDs
|
||||||
|
* @param afterId - JWT ID to check for changes after (from lastAckedStarredPlanChangesJwtId)
|
||||||
|
* @returns { data: Array<PlanSummaryAndPreviousClaim>, hitLimit: boolean }
|
||||||
|
*/
|
||||||
|
export async function getStarredProjectsWithChanges(
|
||||||
|
axios: Axios,
|
||||||
|
apiServer: string,
|
||||||
|
activeDid: string,
|
||||||
|
starredPlanHandleIds: string[],
|
||||||
|
afterId?: string,
|
||||||
|
): Promise<{ data: Array<PlanSummaryAndPreviousClaim>; hitLimit: boolean }> {
|
||||||
|
if (!starredPlanHandleIds || starredPlanHandleIds.length === 0) {
|
||||||
|
return { data: [], hitLimit: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!afterId) {
|
||||||
|
return { data: [], hitLimit: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use POST method for larger lists of project IDs
|
||||||
|
const url = `${apiServer}/api/v2/report/plansLastUpdatedBetween`;
|
||||||
|
const headers = await getHeaders(activeDid);
|
||||||
|
|
||||||
|
const requestBody = {
|
||||||
|
planIds: starredPlanHandleIds,
|
||||||
|
afterId: afterId,
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await axios.post(url, requestBody, { headers });
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Construct GiveAction VC for submission to server
|
* Construct GiveAction VC for submission to server
|
||||||
*
|
*
|
||||||
@@ -1313,28 +1372,6 @@ export const capitalizeAndInsertSpacesBeforeCaps = (text: string) => {
|
|||||||
: text[0].toUpperCase() + text.substr(1).replace(/([A-Z])/g, " $1");
|
: text[0].toUpperCase() + text.substr(1).replace(/([A-Z])/g, " $1");
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Formats type string for display by adding spaces before capitals
|
|
||||||
* and optionally adds an appropriate article prefix (a/an)
|
|
||||||
*
|
|
||||||
* @param text - Text to format
|
|
||||||
* @returns Formatted string with article prefix
|
|
||||||
*/
|
|
||||||
export const capitalizeAndInsertSpacesBeforeCapsWithAPrefix = (
|
|
||||||
text: string,
|
|
||||||
): string => {
|
|
||||||
const word = capitalizeAndInsertSpacesBeforeCaps(text);
|
|
||||||
if (word) {
|
|
||||||
// if the word starts with a vowel, use "an" instead of "a"
|
|
||||||
const firstLetter = word[0].toLowerCase();
|
|
||||||
const vowels = ["a", "e", "i", "o", "u"];
|
|
||||||
const particle = vowels.includes(firstLetter) ? "an" : "a";
|
|
||||||
return particle + " " + word;
|
|
||||||
} else {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
return readable summary of claim, or something generic
|
return readable summary of claim, or something generic
|
||||||
|
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ import {
|
|||||||
faSquareCaretDown,
|
faSquareCaretDown,
|
||||||
faSquareCaretUp,
|
faSquareCaretUp,
|
||||||
faSquarePlus,
|
faSquarePlus,
|
||||||
|
faStar,
|
||||||
faThumbtack,
|
faThumbtack,
|
||||||
faTrashCan,
|
faTrashCan,
|
||||||
faTriangleExclamation,
|
faTriangleExclamation,
|
||||||
@@ -94,6 +95,9 @@ import {
|
|||||||
faXmark,
|
faXmark,
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
|
|
||||||
|
// these are referenced differently, eg. ":icon='['far', 'star']'" as in ProjectViewView.vue
|
||||||
|
import { faStar as faStarRegular } from "@fortawesome/free-regular-svg-icons";
|
||||||
|
|
||||||
// Initialize Font Awesome library with all required icons
|
// Initialize Font Awesome library with all required icons
|
||||||
library.add(
|
library.add(
|
||||||
faArrowDown,
|
faArrowDown,
|
||||||
@@ -168,14 +172,16 @@ library.add(
|
|||||||
faPlus,
|
faPlus,
|
||||||
faQrcode,
|
faQrcode,
|
||||||
faQuestion,
|
faQuestion,
|
||||||
faRotate,
|
|
||||||
faRightFromBracket,
|
faRightFromBracket,
|
||||||
|
faRotate,
|
||||||
faShareNodes,
|
faShareNodes,
|
||||||
faSpinner,
|
faSpinner,
|
||||||
faSquare,
|
faSquare,
|
||||||
faSquareCaretDown,
|
faSquareCaretDown,
|
||||||
faSquareCaretUp,
|
faSquareCaretUp,
|
||||||
faSquarePlus,
|
faSquarePlus,
|
||||||
|
faStar,
|
||||||
|
faStarRegular,
|
||||||
faThumbtack,
|
faThumbtack,
|
||||||
faTrashCan,
|
faTrashCan,
|
||||||
faTriangleExclamation,
|
faTriangleExclamation,
|
||||||
|
|||||||
193
src/libs/util.ts
193
src/libs/util.ts
@@ -3,7 +3,7 @@
|
|||||||
import axios, { AxiosResponse } from "axios";
|
import axios, { AxiosResponse } from "axios";
|
||||||
import { Buffer } from "buffer";
|
import { Buffer } from "buffer";
|
||||||
import * as R from "ramda";
|
import * as R from "ramda";
|
||||||
import { copyToClipboard } from "../services/ClipboardService";
|
import { useClipboard } from "@vueuse/core";
|
||||||
|
|
||||||
import { DEFAULT_PUSH_SERVER, NotificationIface } from "../constants/app";
|
import { DEFAULT_PUSH_SERVER, NotificationIface } from "../constants/app";
|
||||||
import { Account, AccountEncrypted } from "../db/tables/accounts";
|
import { Account, AccountEncrypted } from "../db/tables/accounts";
|
||||||
@@ -160,41 +160,6 @@ export const isGiveAction = (
|
|||||||
return isGiveClaimType(veriClaim.claimType);
|
return isGiveClaimType(veriClaim.claimType);
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface OfferFulfillment {
|
|
||||||
offerHandleId: string;
|
|
||||||
offerType: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extract offer fulfillment information from the fulfills field
|
|
||||||
* Handles both array and single object cases
|
|
||||||
*/
|
|
||||||
export const extractOfferFulfillment = (fulfills: any): OfferFulfillment | null => {
|
|
||||||
if (!fulfills) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle both array and single object cases
|
|
||||||
let offerFulfill = null;
|
|
||||||
|
|
||||||
if (Array.isArray(fulfills)) {
|
|
||||||
// Find the Offer in the fulfills array
|
|
||||||
offerFulfill = fulfills.find((item) => item["@type"] === "Offer");
|
|
||||||
} else if (fulfills["@type"] === "Offer") {
|
|
||||||
// fulfills is a single Offer object
|
|
||||||
offerFulfill = fulfills;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (offerFulfill) {
|
|
||||||
return {
|
|
||||||
offerHandleId: offerFulfill.identifier,
|
|
||||||
offerType: offerFulfill["@type"],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const shortDid = (did: string) => {
|
export const shortDid = (did: string) => {
|
||||||
if (did.startsWith("did:peer:")) {
|
if (did.startsWith("did:peer:")) {
|
||||||
return (
|
return (
|
||||||
@@ -232,19 +197,11 @@ export const nameForContact = (
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const doCopyTwoSecRedo = async (
|
export const doCopyTwoSecRedo = (text: string, fn: () => void) => {
|
||||||
text: string,
|
|
||||||
fn: () => void,
|
|
||||||
): Promise<void> => {
|
|
||||||
fn();
|
fn();
|
||||||
try {
|
useClipboard()
|
||||||
await copyToClipboard(text);
|
.copy(text)
|
||||||
setTimeout(fn, 2000);
|
.then(() => setTimeout(fn, 2000));
|
||||||
} catch (error) {
|
|
||||||
// Note: This utility function doesn't have access to notification system
|
|
||||||
// The calling component should handle error notifications
|
|
||||||
// Error is silently caught to avoid breaking the 2-second redo pattern
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface ConfirmerData {
|
export interface ConfirmerData {
|
||||||
@@ -657,64 +614,57 @@ export const retrieveAllAccountsMetadata = async (): Promise<
|
|||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DUPLICATE_ACCOUNT_ERROR = "Cannot import duplicate account.";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Saves a new identity to SQL database
|
* Saves a new identity to both SQL and Dexie databases
|
||||||
*/
|
*/
|
||||||
export async function saveNewIdentity(
|
export async function saveNewIdentity(
|
||||||
identity: IIdentifier,
|
identity: IIdentifier,
|
||||||
mnemonic: string,
|
mnemonic: string,
|
||||||
derivationPath: string,
|
derivationPath: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
// add to the new sql db
|
try {
|
||||||
const platformService = await getPlatformService();
|
// add to the new sql db
|
||||||
|
const platformService = await getPlatformService();
|
||||||
|
|
||||||
// Check if account already exists before attempting to save
|
const secrets = await platformService.dbQuery(
|
||||||
const existingAccount = await platformService.dbQuery(
|
`SELECT secretBase64 FROM secret`,
|
||||||
"SELECT did FROM accounts WHERE did = ?",
|
);
|
||||||
[identity.did],
|
if (!secrets?.values?.length || !secrets.values[0]?.length) {
|
||||||
);
|
throw new Error(
|
||||||
|
"No initial encryption supported. We recommend you clear your data and start over.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (existingAccount?.values?.length) {
|
const secretBase64 = secrets.values[0][0] as string;
|
||||||
|
|
||||||
|
const secret = base64ToArrayBuffer(secretBase64);
|
||||||
|
const identityStr = JSON.stringify(identity);
|
||||||
|
const encryptedIdentity = await simpleEncrypt(identityStr, secret);
|
||||||
|
const encryptedMnemonic = await simpleEncrypt(mnemonic, secret);
|
||||||
|
const encryptedIdentityBase64 = arrayBufferToBase64(encryptedIdentity);
|
||||||
|
const encryptedMnemonicBase64 = arrayBufferToBase64(encryptedMnemonic);
|
||||||
|
|
||||||
|
const sql = `INSERT INTO accounts (dateCreated, derivationPath, did, identityEncrBase64, mnemonicEncrBase64, publicKeyHex)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?)`;
|
||||||
|
const params = [
|
||||||
|
new Date().toISOString(),
|
||||||
|
derivationPath,
|
||||||
|
identity.did,
|
||||||
|
encryptedIdentityBase64,
|
||||||
|
encryptedMnemonicBase64,
|
||||||
|
identity.keys[0].publicKeyHex,
|
||||||
|
];
|
||||||
|
await platformService.dbExec(sql, params);
|
||||||
|
|
||||||
|
await platformService.updateDefaultSettings({ activeDid: identity.did });
|
||||||
|
|
||||||
|
await platformService.insertNewDidIntoSettings(identity.did);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Failed to update default settings:", error);
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Account with DID ${identity.did} already exists. ${DUPLICATE_ACCOUNT_ERROR}`,
|
"Failed to set default settings. Please try again or restart the app.",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const secrets = await platformService.dbQuery(
|
|
||||||
`SELECT secretBase64 FROM secret`,
|
|
||||||
);
|
|
||||||
if (!secrets?.values?.length || !secrets.values[0]?.length) {
|
|
||||||
throw new Error(
|
|
||||||
"No initial encryption supported. We recommend you clear your data and start over.",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const secretBase64 = secrets.values[0][0] as string;
|
|
||||||
|
|
||||||
const secret = base64ToArrayBuffer(secretBase64);
|
|
||||||
const identityStr = JSON.stringify(identity);
|
|
||||||
const encryptedIdentity = await simpleEncrypt(identityStr, secret);
|
|
||||||
const encryptedMnemonic = await simpleEncrypt(mnemonic, secret);
|
|
||||||
const encryptedIdentityBase64 = arrayBufferToBase64(encryptedIdentity);
|
|
||||||
const encryptedMnemonicBase64 = arrayBufferToBase64(encryptedMnemonic);
|
|
||||||
|
|
||||||
const sql = `INSERT INTO accounts (dateCreated, derivationPath, did, identityEncrBase64, mnemonicEncrBase64, publicKeyHex)
|
|
||||||
VALUES (?, ?, ?, ?, ?, ?)`;
|
|
||||||
const params = [
|
|
||||||
new Date().toISOString(),
|
|
||||||
derivationPath,
|
|
||||||
identity.did,
|
|
||||||
encryptedIdentityBase64,
|
|
||||||
encryptedMnemonicBase64,
|
|
||||||
identity.keys[0].publicKeyHex,
|
|
||||||
];
|
|
||||||
await platformService.dbExec(sql, params);
|
|
||||||
|
|
||||||
await platformService.updateDefaultSettings({ activeDid: identity.did });
|
|
||||||
|
|
||||||
await platformService.insertNewDidIntoSettings(identity.did);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1082,58 +1032,3 @@ export async function importFromMnemonic(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if an account with the given DID already exists in the database
|
|
||||||
*
|
|
||||||
* @param did - The DID to check for duplicates
|
|
||||||
* @returns Promise<boolean> - True if account already exists, false otherwise
|
|
||||||
* @throws Error if database query fails
|
|
||||||
*/
|
|
||||||
export async function checkForDuplicateAccount(did: string): Promise<boolean>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if an account with the given DID already exists in the database
|
|
||||||
*
|
|
||||||
* @param mnemonic - The mnemonic phrase to derive DID from
|
|
||||||
* @param derivationPath - The derivation path to use
|
|
||||||
* @returns Promise<boolean> - True if account already exists, false otherwise
|
|
||||||
* @throws Error if database query fails
|
|
||||||
*/
|
|
||||||
export async function checkForDuplicateAccount(
|
|
||||||
mnemonic: string,
|
|
||||||
derivationPath: string,
|
|
||||||
): Promise<boolean>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Implementation of checkForDuplicateAccount with overloaded signatures
|
|
||||||
*/
|
|
||||||
export async function checkForDuplicateAccount(
|
|
||||||
didOrMnemonic: string,
|
|
||||||
derivationPath?: string,
|
|
||||||
): Promise<boolean> {
|
|
||||||
let didToCheck: string;
|
|
||||||
|
|
||||||
if (derivationPath) {
|
|
||||||
// Derive the DID from mnemonic and derivation path
|
|
||||||
const [address, privateHex, publicHex] = deriveAddress(
|
|
||||||
didOrMnemonic.trim().toLowerCase(),
|
|
||||||
derivationPath,
|
|
||||||
);
|
|
||||||
|
|
||||||
const newId = newIdentifier(address, privateHex, publicHex, derivationPath);
|
|
||||||
didToCheck = newId.did;
|
|
||||||
} else {
|
|
||||||
// Use the provided DID directly
|
|
||||||
didToCheck = didOrMnemonic;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if an account with this DID already exists
|
|
||||||
const platformService = await getPlatformService();
|
|
||||||
const existingAccount = await platformService.dbQuery(
|
|
||||||
"SELECT did FROM accounts WHERE did = ?",
|
|
||||||
[didToCheck],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (existingAccount?.values?.length ?? 0) > 0;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export class WebInlineQRScanner implements QRScannerService {
|
|||||||
// Generate a short random ID for this scanner instance
|
// Generate a short random ID for this scanner instance
|
||||||
this.id = Math.random().toString(36).substring(2, 8).toUpperCase();
|
this.id = Math.random().toString(36).substring(2, 8).toUpperCase();
|
||||||
this.options = options ?? {};
|
this.options = options ?? {};
|
||||||
logger.debug(
|
logger.error(
|
||||||
`[WebInlineQRScanner:${this.id}] Initializing scanner with options:`,
|
`[WebInlineQRScanner:${this.id}] Initializing scanner with options:`,
|
||||||
{
|
{
|
||||||
...this.options,
|
...this.options,
|
||||||
@@ -49,7 +49,7 @@ export class WebInlineQRScanner implements QRScannerService {
|
|||||||
this.context = this.canvas.getContext("2d", { willReadFrequently: true });
|
this.context = this.canvas.getContext("2d", { willReadFrequently: true });
|
||||||
this.video = document.createElement("video");
|
this.video = document.createElement("video");
|
||||||
this.video.setAttribute("playsinline", "true"); // Required for iOS
|
this.video.setAttribute("playsinline", "true"); // Required for iOS
|
||||||
logger.debug(
|
logger.error(
|
||||||
`[WebInlineQRScanner:${this.id}] DOM elements created successfully`,
|
`[WebInlineQRScanner:${this.id}] DOM elements created successfully`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -60,7 +60,7 @@ export class WebInlineQRScanner implements QRScannerService {
|
|||||||
this.cameraStateListeners.forEach((listener) => {
|
this.cameraStateListeners.forEach((listener) => {
|
||||||
try {
|
try {
|
||||||
listener.onStateChange(state, message);
|
listener.onStateChange(state, message);
|
||||||
logger.debug(
|
logger.info(
|
||||||
`[WebInlineQRScanner:${this.id}] Camera state changed to: ${state}`,
|
`[WebInlineQRScanner:${this.id}] Camera state changed to: ${state}`,
|
||||||
{
|
{
|
||||||
state,
|
state,
|
||||||
@@ -89,7 +89,7 @@ export class WebInlineQRScanner implements QRScannerService {
|
|||||||
async checkPermissions(): Promise<boolean> {
|
async checkPermissions(): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
this.updateCameraState("initializing", "Checking camera permissions...");
|
this.updateCameraState("initializing", "Checking camera permissions...");
|
||||||
logger.debug(
|
logger.error(
|
||||||
`[WebInlineQRScanner:${this.id}] Checking camera permissions...`,
|
`[WebInlineQRScanner:${this.id}] Checking camera permissions...`,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -99,7 +99,7 @@ export class WebInlineQRScanner implements QRScannerService {
|
|||||||
const permissions = await navigator.permissions.query({
|
const permissions = await navigator.permissions.query({
|
||||||
name: "camera" as PermissionName,
|
name: "camera" as PermissionName,
|
||||||
});
|
});
|
||||||
logger.debug(
|
logger.error(
|
||||||
`[WebInlineQRScanner:${this.id}] Permission state from Permissions API:`,
|
`[WebInlineQRScanner:${this.id}] Permission state from Permissions API:`,
|
||||||
permissions.state,
|
permissions.state,
|
||||||
);
|
);
|
||||||
@@ -165,7 +165,7 @@ export class WebInlineQRScanner implements QRScannerService {
|
|||||||
"initializing",
|
"initializing",
|
||||||
"Requesting camera permissions...",
|
"Requesting camera permissions...",
|
||||||
);
|
);
|
||||||
logger.debug(
|
logger.error(
|
||||||
`[WebInlineQRScanner:${this.id}] Requesting camera permissions...`,
|
`[WebInlineQRScanner:${this.id}] Requesting camera permissions...`,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -175,7 +175,7 @@ export class WebInlineQRScanner implements QRScannerService {
|
|||||||
(device) => device.kind === "videoinput",
|
(device) => device.kind === "videoinput",
|
||||||
);
|
);
|
||||||
|
|
||||||
logger.debug(`[WebInlineQRScanner:${this.id}] Found video devices:`, {
|
logger.error(`[WebInlineQRScanner:${this.id}] Found video devices:`, {
|
||||||
count: videoDevices.length,
|
count: videoDevices.length,
|
||||||
devices: videoDevices.map((d) => ({ id: d.deviceId, label: d.label })),
|
devices: videoDevices.map((d) => ({ id: d.deviceId, label: d.label })),
|
||||||
userAgent: navigator.userAgent,
|
userAgent: navigator.userAgent,
|
||||||
@@ -188,7 +188,7 @@ export class WebInlineQRScanner implements QRScannerService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Try to get a stream with specific constraints
|
// Try to get a stream with specific constraints
|
||||||
logger.debug(
|
logger.error(
|
||||||
`[WebInlineQRScanner:${this.id}] Requesting camera stream with constraints:`,
|
`[WebInlineQRScanner:${this.id}] Requesting camera stream with constraints:`,
|
||||||
{
|
{
|
||||||
facingMode: "environment",
|
facingMode: "environment",
|
||||||
@@ -210,7 +210,7 @@ export class WebInlineQRScanner implements QRScannerService {
|
|||||||
|
|
||||||
// Stop the test stream immediately
|
// Stop the test stream immediately
|
||||||
stream.getTracks().forEach((track) => {
|
stream.getTracks().forEach((track) => {
|
||||||
logger.debug(`[WebInlineQRScanner:${this.id}] Stopping test track:`, {
|
logger.error(`[WebInlineQRScanner:${this.id}] Stopping test track:`, {
|
||||||
kind: track.kind,
|
kind: track.kind,
|
||||||
label: track.label,
|
label: track.label,
|
||||||
readyState: track.readyState,
|
readyState: track.readyState,
|
||||||
@@ -275,12 +275,12 @@ export class WebInlineQRScanner implements QRScannerService {
|
|||||||
|
|
||||||
async isSupported(): Promise<boolean> {
|
async isSupported(): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
logger.debug(
|
logger.error(
|
||||||
`[WebInlineQRScanner:${this.id}] Checking browser support...`,
|
`[WebInlineQRScanner:${this.id}] Checking browser support...`,
|
||||||
);
|
);
|
||||||
// Check for secure context first
|
// Check for secure context first
|
||||||
if (!window.isSecureContext) {
|
if (!window.isSecureContext) {
|
||||||
logger.debug(
|
logger.error(
|
||||||
`[WebInlineQRScanner:${this.id}] Camera access requires HTTPS (secure context)`,
|
`[WebInlineQRScanner:${this.id}] Camera access requires HTTPS (secure context)`,
|
||||||
);
|
);
|
||||||
return false;
|
return false;
|
||||||
@@ -300,7 +300,7 @@ export class WebInlineQRScanner implements QRScannerService {
|
|||||||
(device) => device.kind === "videoinput",
|
(device) => device.kind === "videoinput",
|
||||||
);
|
);
|
||||||
|
|
||||||
logger.debug(`[WebInlineQRScanner:${this.id}] Device support check:`, {
|
logger.error(`[WebInlineQRScanner:${this.id}] Device support check:`, {
|
||||||
hasSecureContext: window.isSecureContext,
|
hasSecureContext: window.isSecureContext,
|
||||||
hasMediaDevices: !!navigator.mediaDevices,
|
hasMediaDevices: !!navigator.mediaDevices,
|
||||||
hasGetUserMedia: !!navigator.mediaDevices?.getUserMedia,
|
hasGetUserMedia: !!navigator.mediaDevices?.getUserMedia,
|
||||||
@@ -379,7 +379,7 @@ export class WebInlineQRScanner implements QRScannerService {
|
|||||||
|
|
||||||
// Log scan attempt every 100 frames or 1 second
|
// Log scan attempt every 100 frames or 1 second
|
||||||
if (this.scanAttempts % 100 === 0 || timeSinceLastScan >= 1000) {
|
if (this.scanAttempts % 100 === 0 || timeSinceLastScan >= 1000) {
|
||||||
logger.debug(`[WebInlineQRScanner:${this.id}] Scanning frame:`, {
|
logger.error(`[WebInlineQRScanner:${this.id}] Scanning frame:`, {
|
||||||
attempt: this.scanAttempts,
|
attempt: this.scanAttempts,
|
||||||
dimensions: {
|
dimensions: {
|
||||||
width: this.canvas.width,
|
width: this.canvas.width,
|
||||||
@@ -421,7 +421,7 @@ export class WebInlineQRScanner implements QRScannerService {
|
|||||||
!code.data ||
|
!code.data ||
|
||||||
code.data.length === 0;
|
code.data.length === 0;
|
||||||
|
|
||||||
logger.debug(`[WebInlineQRScanner:${this.id}] QR Code detected:`, {
|
logger.error(`[WebInlineQRScanner:${this.id}] QR Code detected:`, {
|
||||||
data: code.data,
|
data: code.data,
|
||||||
location: code.location,
|
location: code.location,
|
||||||
attempts: this.scanAttempts,
|
attempts: this.scanAttempts,
|
||||||
@@ -512,13 +512,13 @@ export class WebInlineQRScanner implements QRScannerService {
|
|||||||
this.scanAttempts = 0;
|
this.scanAttempts = 0;
|
||||||
this.lastScanTime = Date.now();
|
this.lastScanTime = Date.now();
|
||||||
this.updateCameraState("initializing", "Starting camera...");
|
this.updateCameraState("initializing", "Starting camera...");
|
||||||
logger.debug(
|
logger.error(
|
||||||
`[WebInlineQRScanner:${this.id}] Starting scan with options:`,
|
`[WebInlineQRScanner:${this.id}] Starting scan with options:`,
|
||||||
this.options,
|
this.options,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Get camera stream with options
|
// Get camera stream with options
|
||||||
logger.debug(
|
logger.error(
|
||||||
`[WebInlineQRScanner:${this.id}] Requesting camera stream...`,
|
`[WebInlineQRScanner:${this.id}] Requesting camera stream...`,
|
||||||
);
|
);
|
||||||
this.stream = await navigator.mediaDevices.getUserMedia({
|
this.stream = await navigator.mediaDevices.getUserMedia({
|
||||||
@@ -531,7 +531,7 @@ export class WebInlineQRScanner implements QRScannerService {
|
|||||||
|
|
||||||
this.updateCameraState("active", "Camera is active");
|
this.updateCameraState("active", "Camera is active");
|
||||||
|
|
||||||
logger.debug(`[WebInlineQRScanner:${this.id}] Camera stream obtained:`, {
|
logger.error(`[WebInlineQRScanner:${this.id}] Camera stream obtained:`, {
|
||||||
tracks: this.stream.getTracks().map((t) => ({
|
tracks: this.stream.getTracks().map((t) => ({
|
||||||
kind: t.kind,
|
kind: t.kind,
|
||||||
label: t.label,
|
label: t.label,
|
||||||
@@ -550,14 +550,14 @@ export class WebInlineQRScanner implements QRScannerService {
|
|||||||
this.video.style.display = "none";
|
this.video.style.display = "none";
|
||||||
}
|
}
|
||||||
await this.video.play();
|
await this.video.play();
|
||||||
logger.debug(
|
logger.error(
|
||||||
`[WebInlineQRScanner:${this.id}] Video element started playing`,
|
`[WebInlineQRScanner:${this.id}] Video element started playing`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Emit stream to component
|
// Emit stream to component
|
||||||
this.events.emit("stream", this.stream);
|
this.events.emit("stream", this.stream);
|
||||||
logger.debug(`[WebInlineQRScanner:${this.id}] Stream event emitted`);
|
logger.error(`[WebInlineQRScanner:${this.id}] Stream event emitted`);
|
||||||
|
|
||||||
// Start QR code scanning
|
// Start QR code scanning
|
||||||
this.scanQRCode();
|
this.scanQRCode();
|
||||||
@@ -595,7 +595,7 @@ export class WebInlineQRScanner implements QRScannerService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
logger.debug(`[WebInlineQRScanner:${this.id}] Stopping scan`, {
|
logger.error(`[WebInlineQRScanner:${this.id}] Stopping scan`, {
|
||||||
scanAttempts: this.scanAttempts,
|
scanAttempts: this.scanAttempts,
|
||||||
duration: Date.now() - this.lastScanTime,
|
duration: Date.now() - this.lastScanTime,
|
||||||
});
|
});
|
||||||
@@ -604,7 +604,7 @@ export class WebInlineQRScanner implements QRScannerService {
|
|||||||
if (this.animationFrameId !== null) {
|
if (this.animationFrameId !== null) {
|
||||||
cancelAnimationFrame(this.animationFrameId);
|
cancelAnimationFrame(this.animationFrameId);
|
||||||
this.animationFrameId = null;
|
this.animationFrameId = null;
|
||||||
logger.debug(
|
logger.error(
|
||||||
`[WebInlineQRScanner:${this.id}] Animation frame cancelled`,
|
`[WebInlineQRScanner:${this.id}] Animation frame cancelled`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -613,13 +613,13 @@ export class WebInlineQRScanner implements QRScannerService {
|
|||||||
if (this.video) {
|
if (this.video) {
|
||||||
this.video.pause();
|
this.video.pause();
|
||||||
this.video.srcObject = null;
|
this.video.srcObject = null;
|
||||||
logger.debug(`[WebInlineQRScanner:${this.id}] Video element stopped`);
|
logger.error(`[WebInlineQRScanner:${this.id}] Video element stopped`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stop all tracks in the stream
|
// Stop all tracks in the stream
|
||||||
if (this.stream) {
|
if (this.stream) {
|
||||||
this.stream.getTracks().forEach((track) => {
|
this.stream.getTracks().forEach((track) => {
|
||||||
logger.debug(`[WebInlineQRScanner:${this.id}] Stopping track:`, {
|
logger.error(`[WebInlineQRScanner:${this.id}] Stopping track:`, {
|
||||||
kind: track.kind,
|
kind: track.kind,
|
||||||
label: track.label,
|
label: track.label,
|
||||||
readyState: track.readyState,
|
readyState: track.readyState,
|
||||||
@@ -631,7 +631,7 @@ export class WebInlineQRScanner implements QRScannerService {
|
|||||||
|
|
||||||
// Emit stream stopped event
|
// Emit stream stopped event
|
||||||
this.events.emit("stream", null);
|
this.events.emit("stream", null);
|
||||||
logger.debug(
|
logger.error(
|
||||||
`[WebInlineQRScanner:${this.id}] Stream stopped event emitted`,
|
`[WebInlineQRScanner:${this.id}] Stream stopped event emitted`,
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -643,17 +643,17 @@ export class WebInlineQRScanner implements QRScannerService {
|
|||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
this.isScanning = false;
|
this.isScanning = false;
|
||||||
logger.debug(`[WebInlineQRScanner:${this.id}] Scan stopped successfully`);
|
logger.error(`[WebInlineQRScanner:${this.id}] Scan stopped successfully`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
addListener(listener: ScanListener): void {
|
addListener(listener: ScanListener): void {
|
||||||
logger.debug(`[WebInlineQRScanner:${this.id}] Adding scan listener`);
|
logger.error(`[WebInlineQRScanner:${this.id}] Adding scan listener`);
|
||||||
this.scanListener = listener;
|
this.scanListener = listener;
|
||||||
}
|
}
|
||||||
|
|
||||||
onStream(callback: (stream: MediaStream | null) => void): void {
|
onStream(callback: (stream: MediaStream | null) => void): void {
|
||||||
logger.debug(
|
logger.error(
|
||||||
`[WebInlineQRScanner:${this.id}] Adding stream event listener`,
|
`[WebInlineQRScanner:${this.id}] Adding stream event listener`,
|
||||||
);
|
);
|
||||||
this.events.on("stream", callback);
|
this.events.on("stream", callback);
|
||||||
@@ -661,24 +661,24 @@ export class WebInlineQRScanner implements QRScannerService {
|
|||||||
|
|
||||||
async cleanup(): Promise<void> {
|
async cleanup(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
logger.debug(`[WebInlineQRScanner:${this.id}] Starting cleanup`);
|
logger.error(`[WebInlineQRScanner:${this.id}] Starting cleanup`);
|
||||||
await this.stopScan();
|
await this.stopScan();
|
||||||
this.events.removeAllListeners();
|
this.events.removeAllListeners();
|
||||||
logger.debug(`[WebInlineQRScanner:${this.id}] Event listeners removed`);
|
logger.error(`[WebInlineQRScanner:${this.id}] Event listeners removed`);
|
||||||
|
|
||||||
// Clean up DOM elements
|
// Clean up DOM elements
|
||||||
if (this.video) {
|
if (this.video) {
|
||||||
this.video.remove();
|
this.video.remove();
|
||||||
this.video = null;
|
this.video = null;
|
||||||
logger.debug(`[WebInlineQRScanner:${this.id}] Video element removed`);
|
logger.error(`[WebInlineQRScanner:${this.id}] Video element removed`);
|
||||||
}
|
}
|
||||||
if (this.canvas) {
|
if (this.canvas) {
|
||||||
this.canvas.remove();
|
this.canvas.remove();
|
||||||
this.canvas = null;
|
this.canvas = null;
|
||||||
logger.debug(`[WebInlineQRScanner:${this.id}] Canvas element removed`);
|
logger.error(`[WebInlineQRScanner:${this.id}] Canvas element removed`);
|
||||||
}
|
}
|
||||||
this.context = null;
|
this.context = null;
|
||||||
logger.debug(
|
logger.error(
|
||||||
`[WebInlineQRScanner:${this.id}] Cleanup completed successfully`,
|
`[WebInlineQRScanner:${this.id}] Cleanup completed successfully`,
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -85,7 +85,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
||||||
import { MASTER_SETTINGS_KEY } from "@/db/tables/settings";
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
mixins: [PlatformServiceMixin],
|
mixins: [PlatformServiceMixin],
|
||||||
@@ -197,10 +196,10 @@ This tests the helper method only - no database interaction`;
|
|||||||
const success = await this.$saveSettings(testSettings);
|
const success = await this.$saveSettings(testSettings);
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
// Now query the raw database to see how it's actually stored
|
// Now query the raw database to see how it's actually stored.
|
||||||
|
// Note that new users probably have settings with ID of 1 but old migrated users might skip to 2.
|
||||||
const rawResult = await this.$dbQuery(
|
const rawResult = await this.$dbQuery(
|
||||||
"SELECT searchBoxes FROM settings WHERE id = ?",
|
"SELECT searchBoxes FROM settings limit 1",
|
||||||
[MASTER_SETTINGS_KEY],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (rawResult?.values?.length) {
|
if (rawResult?.values?.length) {
|
||||||
|
|||||||
@@ -249,13 +249,13 @@ export const PlatformServiceMixin = {
|
|||||||
// Keep null values as null
|
// Keep null values as null
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle JSON fields like contactMethods
|
// Convert SQLite JSON strings to objects/arrays
|
||||||
if (column === "contactMethods" && typeof value === "string") {
|
if (
|
||||||
try {
|
column === "contactMethods" ||
|
||||||
value = JSON.parse(value);
|
column === "searchBoxes" ||
|
||||||
} catch {
|
column === "starredPlanHandleIds"
|
||||||
value = [];
|
) {
|
||||||
}
|
value = this._parseJsonField(value, []);
|
||||||
}
|
}
|
||||||
|
|
||||||
obj[column] = value;
|
obj[column] = value;
|
||||||
|
|||||||
@@ -1,90 +0,0 @@
|
|||||||
import { NotificationIface } from "@/constants/app";
|
|
||||||
|
|
||||||
const SEED_REMINDER_KEY = "seedPhraseReminderLastShown";
|
|
||||||
const REMINDER_COOLDOWN_MS = 24 * 60 * 60 * 1000; // 24 hours in milliseconds
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if the seed phrase backup reminder should be shown
|
|
||||||
* @param hasBackedUpSeed - Whether the user has backed up their seed phrase
|
|
||||||
* @returns true if the reminder should be shown, false otherwise
|
|
||||||
*/
|
|
||||||
export function shouldShowSeedReminder(hasBackedUpSeed: boolean): boolean {
|
|
||||||
// Don't show if user has already backed up
|
|
||||||
if (hasBackedUpSeed) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check localStorage for last shown time
|
|
||||||
const lastShown = localStorage.getItem(SEED_REMINDER_KEY);
|
|
||||||
if (!lastShown) {
|
|
||||||
return true; // First time, show the reminder
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const lastShownTime = parseInt(lastShown, 10);
|
|
||||||
const now = Date.now();
|
|
||||||
const timeSinceLastShown = now - lastShownTime;
|
|
||||||
|
|
||||||
// Show if more than 24 hours have passed
|
|
||||||
return timeSinceLastShown >= REMINDER_COOLDOWN_MS;
|
|
||||||
} catch (error) {
|
|
||||||
// If there's an error parsing the timestamp, show the reminder
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Marks the seed phrase reminder as shown by updating localStorage
|
|
||||||
*/
|
|
||||||
export function markSeedReminderShown(): void {
|
|
||||||
localStorage.setItem(SEED_REMINDER_KEY, Date.now().toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates the seed phrase backup reminder notification
|
|
||||||
* @returns NotificationIface configuration for the reminder modal
|
|
||||||
*/
|
|
||||||
export function createSeedReminderNotification(): NotificationIface {
|
|
||||||
return {
|
|
||||||
group: "modal",
|
|
||||||
type: "confirm",
|
|
||||||
title: "Backup Your Identifier Seed?",
|
|
||||||
text: "It looks like you haven't backed up your identifier seed yet. It's important to back it up as soon as possible to secure your identity.",
|
|
||||||
yesText: "Backup Identifier Seed",
|
|
||||||
noText: "Remind me Later",
|
|
||||||
onYes: async () => {
|
|
||||||
// Navigate to seed backup page
|
|
||||||
window.location.href = "/seed-backup";
|
|
||||||
},
|
|
||||||
onNo: async () => {
|
|
||||||
// Mark as shown so it won't appear again for 24 hours
|
|
||||||
markSeedReminderShown();
|
|
||||||
},
|
|
||||||
onCancel: async () => {
|
|
||||||
// Mark as shown so it won't appear again for 24 hours
|
|
||||||
markSeedReminderShown();
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shows the seed phrase backup reminder if conditions are met
|
|
||||||
* @param hasBackedUpSeed - Whether the user has backed up their seed phrase
|
|
||||||
* @param notifyFunction - Function to show notifications
|
|
||||||
* @returns true if the reminder was shown, false otherwise
|
|
||||||
*/
|
|
||||||
export function showSeedPhraseReminder(
|
|
||||||
hasBackedUpSeed: boolean,
|
|
||||||
notifyFunction: (notification: NotificationIface, timeout?: number) => void,
|
|
||||||
): boolean {
|
|
||||||
if (shouldShowSeedReminder(hasBackedUpSeed)) {
|
|
||||||
const notification = createSeedReminderNotification();
|
|
||||||
// Add 1-second delay before showing the modal to allow success message to be visible
|
|
||||||
setTimeout(() => {
|
|
||||||
// Pass -1 as timeout to ensure modal stays open until user interaction
|
|
||||||
notifyFunction(notification, -1);
|
|
||||||
}, 1000);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
@@ -150,8 +150,6 @@
|
|||||||
</section>
|
</section>
|
||||||
<PushNotificationPermission ref="pushNotificationPermission" />
|
<PushNotificationPermission ref="pushNotificationPermission" />
|
||||||
|
|
||||||
<LocationSearchSection :search-box="searchBox" />
|
|
||||||
|
|
||||||
<!-- User Profile -->
|
<!-- User Profile -->
|
||||||
<section
|
<section
|
||||||
v-if="isRegistered"
|
v-if="isRegistered"
|
||||||
@@ -244,6 +242,8 @@
|
|||||||
<div v-else>Saving...</div>
|
<div v-else>Saving...</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<LocationSearchSection :search-box="searchBox" />
|
||||||
|
|
||||||
<UsageLimitsSection
|
<UsageLimitsSection
|
||||||
v-if="activeDid"
|
v-if="activeDid"
|
||||||
:loading-limits="loadingLimits"
|
:loading-limits="loadingLimits"
|
||||||
@@ -764,7 +764,7 @@ import { IIdentifier } from "@veramo/core";
|
|||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
import { RouteLocationNormalizedLoaded, Router } from "vue-router";
|
import { RouteLocationNormalizedLoaded, Router } from "vue-router";
|
||||||
import { copyToClipboard } from "../services/ClipboardService";
|
import { useClipboard } from "@vueuse/core";
|
||||||
import { LMap, LMarker, LTileLayer } from "@vue-leaflet/vue-leaflet";
|
import { LMap, LMarker, LTileLayer } from "@vue-leaflet/vue-leaflet";
|
||||||
import { Capacitor } from "@capacitor/core";
|
import { Capacitor } from "@capacitor/core";
|
||||||
|
|
||||||
@@ -811,7 +811,6 @@ import { logger } from "../utils/logger";
|
|||||||
import { PlatformServiceMixin } from "../utils/PlatformServiceMixin";
|
import { PlatformServiceMixin } from "../utils/PlatformServiceMixin";
|
||||||
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
|
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
|
||||||
import { ACCOUNT_VIEW_CONSTANTS } from "@/constants/accountView";
|
import { ACCOUNT_VIEW_CONSTANTS } from "@/constants/accountView";
|
||||||
import { showSeedPhraseReminder } from "@/utils/seedPhraseReminder";
|
|
||||||
import {
|
import {
|
||||||
AccountSettings,
|
AccountSettings,
|
||||||
isApiError,
|
isApiError,
|
||||||
@@ -1060,8 +1059,8 @@ export default class AccountViewView extends Vue {
|
|||||||
this.hideRegisterPromptOnNewContact =
|
this.hideRegisterPromptOnNewContact =
|
||||||
!!settings.hideRegisterPromptOnNewContact;
|
!!settings.hideRegisterPromptOnNewContact;
|
||||||
this.isRegistered = !!settings?.isRegistered;
|
this.isRegistered = !!settings?.isRegistered;
|
||||||
this.isSearchAreasSet = !!settings.searchBoxes;
|
this.isSearchAreasSet =
|
||||||
this.searchBox = settings.searchBoxes?.[0] || null;
|
!!settings.searchBoxes && settings.searchBoxes.length > 0;
|
||||||
this.notifyingNewActivity = !!settings.notifyingNewActivityTime;
|
this.notifyingNewActivity = !!settings.notifyingNewActivityTime;
|
||||||
this.notifyingNewActivityTime = settings.notifyingNewActivityTime || "";
|
this.notifyingNewActivityTime = settings.notifyingNewActivityTime || "";
|
||||||
this.notifyingReminder = !!settings.notifyingReminderTime;
|
this.notifyingReminder = !!settings.notifyingReminderTime;
|
||||||
@@ -1075,6 +1074,7 @@ export default class AccountViewView extends Vue {
|
|||||||
this.passkeyExpirationMinutes =
|
this.passkeyExpirationMinutes =
|
||||||
settings.passkeyExpirationMinutes ?? DEFAULT_PASSKEY_EXPIRATION_MINUTES;
|
settings.passkeyExpirationMinutes ?? DEFAULT_PASSKEY_EXPIRATION_MINUTES;
|
||||||
this.previousPasskeyExpirationMinutes = this.passkeyExpirationMinutes;
|
this.previousPasskeyExpirationMinutes = this.passkeyExpirationMinutes;
|
||||||
|
this.searchBox = settings.searchBoxes?.[0] || null;
|
||||||
this.showGeneralAdvanced = !!settings.showGeneralAdvanced;
|
this.showGeneralAdvanced = !!settings.showGeneralAdvanced;
|
||||||
this.showShortcutBvc = !!settings.showShortcutBvc;
|
this.showShortcutBvc = !!settings.showShortcutBvc;
|
||||||
this.warnIfProdServer = !!settings.warnIfProdServer;
|
this.warnIfProdServer = !!settings.warnIfProdServer;
|
||||||
@@ -1084,15 +1084,11 @@ export default class AccountViewView extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// call fn, copy text to the clipboard, then redo fn after 2 seconds
|
// call fn, copy text to the clipboard, then redo fn after 2 seconds
|
||||||
async doCopyTwoSecRedo(text: string, fn: () => void): Promise<void> {
|
doCopyTwoSecRedo(text: string, fn: () => void): void {
|
||||||
fn();
|
fn();
|
||||||
try {
|
useClipboard()
|
||||||
await copyToClipboard(text);
|
.copy(text)
|
||||||
setTimeout(fn, 2000);
|
.then(() => setTimeout(fn, 2000));
|
||||||
} catch (error) {
|
|
||||||
this.$logAndConsole(`Error copying to clipboard: ${error}`, true);
|
|
||||||
this.notify.error("Failed to copy to clipboard.");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async toggleShowContactAmounts(): Promise<void> {
|
async toggleShowContactAmounts(): Promise<void> {
|
||||||
@@ -1700,14 +1696,6 @@ export default class AccountViewView extends Vue {
|
|||||||
);
|
);
|
||||||
if (success) {
|
if (success) {
|
||||||
this.notify.success(ACCOUNT_VIEW_CONSTANTS.SUCCESS.PROFILE_SAVED);
|
this.notify.success(ACCOUNT_VIEW_CONSTANTS.SUCCESS.PROFILE_SAVED);
|
||||||
|
|
||||||
// Show seed phrase backup reminder if needed
|
|
||||||
try {
|
|
||||||
const settings = await this.$accountSettings();
|
|
||||||
showSeedPhraseReminder(!!settings.hasBackedUpSeed, this.$notify);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error("Error checking seed backup status:", error);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
this.notify.error(ACCOUNT_VIEW_CONSTANTS.ERRORS.PROFILE_SAVE_ERROR);
|
this.notify.error(ACCOUNT_VIEW_CONSTANTS.ERRORS.PROFILE_SAVE_ERROR);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,7 +41,6 @@ import { Router, RouteLocationNormalizedLoaded } from "vue-router";
|
|||||||
import { logger } from "../utils/logger";
|
import { logger } from "../utils/logger";
|
||||||
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
||||||
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
|
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
|
||||||
import { showSeedPhraseReminder } from "@/utils/seedPhraseReminder";
|
|
||||||
|
|
||||||
// Type guard for API responses
|
// Type guard for API responses
|
||||||
function isApiResponse(response: unknown): response is AxiosResponse {
|
function isApiResponse(response: unknown): response is AxiosResponse {
|
||||||
@@ -224,14 +223,6 @@ export default class ClaimAddRawView extends Vue {
|
|||||||
);
|
);
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
this.notify.success("Claim submitted.", TIMEOUTS.LONG);
|
this.notify.success("Claim submitted.", TIMEOUTS.LONG);
|
||||||
|
|
||||||
// Show seed phrase backup reminder if needed
|
|
||||||
try {
|
|
||||||
const settings = await this.$accountSettings();
|
|
||||||
showSeedPhraseReminder(!!settings.hasBackedUpSeed, this.$notify);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error("Error checking seed backup status:", error);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
logger.error("Got error submitting the claim:", result);
|
logger.error("Got error submitting the claim:", result);
|
||||||
this.notify.error(
|
this.notify.error(
|
||||||
|
|||||||
@@ -24,9 +24,7 @@
|
|||||||
<div class="flex columns-3">
|
<div class="flex columns-3">
|
||||||
<h2 class="text-md font-bold w-full">
|
<h2 class="text-md font-bold w-full">
|
||||||
{{
|
{{
|
||||||
serverUtil.capitalizeAndInsertSpacesBeforeCaps(
|
capitalizeAndInsertSpacesBeforeCaps(veriClaim.claimType || "")
|
||||||
veriClaim.claimType || "",
|
|
||||||
)
|
|
||||||
}}
|
}}
|
||||||
<button
|
<button
|
||||||
v-if="canEditClaim"
|
v-if="canEditClaim"
|
||||||
@@ -58,7 +56,7 @@
|
|||||||
title="Copy Printable Certificate Link"
|
title="Copy Printable Certificate Link"
|
||||||
aria-label="Copy printable certificate link"
|
aria-label="Copy printable certificate link"
|
||||||
@click="
|
@click="
|
||||||
copyTextToClipboard(
|
copyToClipboard(
|
||||||
'A link to the certificate page',
|
'A link to the certificate page',
|
||||||
`${APP_SERVER}/deep-link/claim-cert/${veriClaim.id}`,
|
`${APP_SERVER}/deep-link/claim-cert/${veriClaim.id}`,
|
||||||
)
|
)
|
||||||
@@ -72,9 +70,7 @@
|
|||||||
<button
|
<button
|
||||||
title="Copy Link"
|
title="Copy Link"
|
||||||
aria-label="Copy page link"
|
aria-label="Copy page link"
|
||||||
@click="
|
@click="copyToClipboard('A link to this page', windowDeepLink)"
|
||||||
copyTextToClipboard('A link to this page', windowDeepLink)
|
|
||||||
"
|
|
||||||
>
|
>
|
||||||
<font-awesome icon="link" class="text-slate-500" />
|
<font-awesome icon="link" class="text-slate-500" />
|
||||||
</button>
|
</button>
|
||||||
@@ -83,7 +79,10 @@
|
|||||||
<div class="text-sm">
|
<div class="text-sm">
|
||||||
<div data-testId="description">
|
<div data-testId="description">
|
||||||
<font-awesome icon="message" class="fa-fw text-slate-400" />
|
<font-awesome icon="message" class="fa-fw text-slate-400" />
|
||||||
{{ claimDescription }}
|
<vue-markdown
|
||||||
|
:source="claimDescription"
|
||||||
|
class="markdown-content"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<font-awesome icon="user" class="fa-fw text-slate-400" />
|
<font-awesome icon="user" class="fa-fw text-slate-400" />
|
||||||
@@ -110,91 +109,77 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Fullfills Links -->
|
<!-- Fullfills Links -->
|
||||||
<div class="mt-4 empty:hidden">
|
|
||||||
<!-- fullfills links for a give -->
|
|
||||||
<div v-if="detailsForGive?.fulfillsPlanHandleId">
|
|
||||||
<router-link
|
|
||||||
:to="
|
|
||||||
'/project/' +
|
|
||||||
encodeURIComponent(detailsForGive?.fulfillsPlanHandleId)
|
|
||||||
"
|
|
||||||
class="text-blue-500 mt-2"
|
|
||||||
>
|
|
||||||
This fulfills a bigger plan
|
|
||||||
<font-awesome
|
|
||||||
icon="arrow-up-right-from-square"
|
|
||||||
class="fa-fw"
|
|
||||||
/>
|
|
||||||
</router-link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Show offer fulfillment if this give fulfills an offer -->
|
<!-- fullfills links for a give -->
|
||||||
<div v-if="detailsForGiveOfferFulfillment?.offerHandleId">
|
<div v-if="detailsForGive?.fulfillsPlanHandleId" class="mt-4">
|
||||||
<!-- router-link to /claim/ only changes URL path -->
|
<router-link
|
||||||
<a
|
:to="
|
||||||
class="text-blue-500 mt-4 cursor-pointer"
|
'/project/' +
|
||||||
@click="
|
encodeURIComponent(detailsForGive?.fulfillsPlanHandleId)
|
||||||
showDifferentClaimPage(
|
"
|
||||||
detailsForGiveOfferFulfillment.offerHandleId,
|
class="text-blue-500 mt-2"
|
||||||
)
|
>
|
||||||
"
|
Fulfills a bigger plan...
|
||||||
>
|
</router-link>
|
||||||
This fulfills
|
</div>
|
||||||
{{
|
<!-- if there's another, it's probably fulfilling an offer, too -->
|
||||||
serverUtil.capitalizeAndInsertSpacesBeforeCapsWithAPrefix(
|
<div
|
||||||
detailsForGiveOfferFulfillment.offerType || "Offer",
|
v-if="
|
||||||
)
|
detailsForGive?.fulfillsType &&
|
||||||
}}
|
detailsForGive?.fulfillsType !== 'PlanAction' &&
|
||||||
<font-awesome
|
detailsForGive?.fulfillsHandleId
|
||||||
icon="arrow-up-right-from-square"
|
"
|
||||||
class="fa-fw"
|
>
|
||||||
/>
|
<!-- router-link to /claim/ only changes URL path -->
|
||||||
</a>
|
<a
|
||||||
</div>
|
class="text-blue-500 mt-4 cursor-pointer"
|
||||||
|
@click="
|
||||||
|
showDifferentClaimPage(detailsForGive?.fulfillsHandleId)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
Fulfills
|
||||||
|
{{
|
||||||
|
capitalizeAndInsertSpacesBeforeCaps(
|
||||||
|
detailsForGive.fulfillsType,
|
||||||
|
)
|
||||||
|
}}...
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- fullfills links for an offer -->
|
<!-- fullfills links for an offer -->
|
||||||
<div v-if="detailsForOffer?.fulfillsPlanHandleId">
|
<div v-if="detailsForOffer?.fulfillsPlanHandleId">
|
||||||
<router-link
|
<router-link
|
||||||
:to="
|
:to="
|
||||||
'/project/' +
|
'/project/' +
|
||||||
encodeURIComponent(detailsForOffer?.fulfillsPlanHandleId)
|
encodeURIComponent(detailsForOffer?.fulfillsPlanHandleId)
|
||||||
"
|
"
|
||||||
class="text-blue-500 mt-4"
|
class="text-blue-500 mt-4"
|
||||||
>
|
>
|
||||||
Offered to a bigger plan
|
Offered to a bigger plan...
|
||||||
<font-awesome
|
</router-link>
|
||||||
icon="arrow-up-right-from-square"
|
</div>
|
||||||
class="fa-fw"
|
|
||||||
/>
|
|
||||||
</router-link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Providers -->
|
<!-- Providers -->
|
||||||
<div v-if="providersForGive?.length > 0">
|
<div v-if="providersForGive?.length > 0" class="mt-4">
|
||||||
<span>Other assistance provided by:</span>
|
<span>Other assistance provided by:</span>
|
||||||
<ul class="ml-4">
|
<ul class="ml-4">
|
||||||
<li
|
<li
|
||||||
v-for="provider of providersForGive"
|
v-for="provider of providersForGive"
|
||||||
:key="provider.identifier"
|
:key="provider.identifier"
|
||||||
class="list-disc ml-4"
|
class="list-disc ml-4"
|
||||||
>
|
>
|
||||||
<div class="flex gap-4">
|
<div class="flex gap-4">
|
||||||
<div class="grow overflow-hidden">
|
<div class="grow overflow-hidden">
|
||||||
<a
|
<a
|
||||||
class="text-blue-500 mt-4 cursor-pointer"
|
class="text-blue-500 mt-4 cursor-pointer"
|
||||||
@click="handleProviderClick(provider)"
|
@click="handleProviderClick(provider)"
|
||||||
>
|
>
|
||||||
an activity
|
an activity...
|
||||||
<font-awesome
|
</a>
|
||||||
icon="arrow-up-right-from-square"
|
|
||||||
class="fa-fw"
|
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</div>
|
||||||
</ul>
|
</li>
|
||||||
</div>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -401,7 +386,7 @@
|
|||||||
contacts can see more details:
|
contacts can see more details:
|
||||||
<a
|
<a
|
||||||
class="text-blue-500"
|
class="text-blue-500"
|
||||||
@click="copyTextToClipboard('A link to this page', windowDeepLink)"
|
@click="copyToClipboard('A link to this page', windowDeepLink)"
|
||||||
>click to copy this page info</a
|
>click to copy this page info</a
|
||||||
>
|
>
|
||||||
and see if they can make an introduction. Someone is connected to
|
and see if they can make an introduction. Someone is connected to
|
||||||
@@ -424,7 +409,7 @@
|
|||||||
If you'd like an introduction,
|
If you'd like an introduction,
|
||||||
<a
|
<a
|
||||||
class="text-blue-500"
|
class="text-blue-500"
|
||||||
@click="copyTextToClipboard('A link to this page', windowDeepLink)"
|
@click="copyToClipboard('A link to this page', windowDeepLink)"
|
||||||
>share this page with them and ask if they'll tell you more about
|
>share this page with them and ask if they'll tell you more about
|
||||||
about the participants.</a
|
about the participants.</a
|
||||||
>
|
>
|
||||||
@@ -533,8 +518,10 @@ import { AxiosError } from "axios";
|
|||||||
import * as yaml from "js-yaml";
|
import * as yaml from "js-yaml";
|
||||||
import * as R from "ramda";
|
import * as R from "ramda";
|
||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
|
import VueMarkdown from "vue-markdown-render";
|
||||||
import { Router, RouteLocationNormalizedLoaded } from "vue-router";
|
import { Router, RouteLocationNormalizedLoaded } from "vue-router";
|
||||||
import { copyToClipboard } from "../services/ClipboardService";
|
import { useClipboard } from "@vueuse/core";
|
||||||
|
|
||||||
import { GenericVerifiableCredential } from "../interfaces";
|
import { GenericVerifiableCredential } from "../interfaces";
|
||||||
import GiftedDialog from "../components/GiftedDialog.vue";
|
import GiftedDialog from "../components/GiftedDialog.vue";
|
||||||
import QuickNav from "../components/QuickNav.vue";
|
import QuickNav from "../components/QuickNav.vue";
|
||||||
@@ -553,7 +540,7 @@ import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
|
|||||||
import { APP_SERVER } from "@/constants/app";
|
import { APP_SERVER } from "@/constants/app";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: { GiftedDialog, QuickNav },
|
components: { GiftedDialog, QuickNav, VueMarkdown },
|
||||||
mixins: [PlatformServiceMixin],
|
mixins: [PlatformServiceMixin],
|
||||||
})
|
})
|
||||||
export default class ClaimView extends Vue {
|
export default class ClaimView extends Vue {
|
||||||
@@ -574,17 +561,6 @@ export default class ClaimView extends Vue {
|
|||||||
fulfillsPlanHandleId?: string;
|
fulfillsPlanHandleId?: string;
|
||||||
fulfillsType?: string;
|
fulfillsType?: string;
|
||||||
fulfillsHandleId?: string;
|
fulfillsHandleId?: string;
|
||||||
fullClaim?: {
|
|
||||||
fulfills?: Array<{
|
|
||||||
"@type": string;
|
|
||||||
identifier?: string;
|
|
||||||
}>;
|
|
||||||
};
|
|
||||||
} | null = null;
|
|
||||||
// Additional offer information extracted from the fulfills array
|
|
||||||
detailsForGiveOfferFulfillment: {
|
|
||||||
offerHandleId?: string;
|
|
||||||
offerType?: string;
|
|
||||||
} | null = null;
|
} | null = null;
|
||||||
detailsForOffer: { fulfillsPlanHandleId?: string } | null = null;
|
detailsForOffer: { fulfillsPlanHandleId?: string } | null = null;
|
||||||
// Project information for fulfillsPlanHandleId
|
// Project information for fulfillsPlanHandleId
|
||||||
@@ -718,7 +694,6 @@ export default class ClaimView extends Vue {
|
|||||||
this.confsVisibleToIdList = [];
|
this.confsVisibleToIdList = [];
|
||||||
this.detailsForGive = null;
|
this.detailsForGive = null;
|
||||||
this.detailsForOffer = null;
|
this.detailsForOffer = null;
|
||||||
this.detailsForGiveOfferFulfillment = null;
|
|
||||||
this.projectInfo = null;
|
this.projectInfo = null;
|
||||||
this.fullClaim = null;
|
this.fullClaim = null;
|
||||||
this.fullClaimDump = "";
|
this.fullClaimDump = "";
|
||||||
@@ -731,15 +706,6 @@ export default class ClaimView extends Vue {
|
|||||||
this.veriClaimDidsVisible = {};
|
this.veriClaimDidsVisible = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Extract offer fulfillment information from the fulfills array
|
|
||||||
*/
|
|
||||||
extractOfferFulfillment() {
|
|
||||||
this.detailsForGiveOfferFulfillment = libsUtil.extractOfferFulfillment(
|
|
||||||
this.detailsForGive?.fullClaim?.fulfills
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// =================================================
|
// =================================================
|
||||||
// UTILITY METHODS
|
// UTILITY METHODS
|
||||||
// =================================================
|
// =================================================
|
||||||
@@ -797,6 +763,13 @@ export default class ClaimView extends Vue {
|
|||||||
this.canShare = !!navigator.share;
|
this.canShare = !!navigator.share;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// insert a space before any capital letters except the initial letter
|
||||||
|
// (and capitalize initial letter, just in case)
|
||||||
|
capitalizeAndInsertSpacesBeforeCaps(text: string): string {
|
||||||
|
if (!text) return "";
|
||||||
|
return text[0].toUpperCase() + text.substr(1).replace(/([A-Z])/g, " $1");
|
||||||
|
}
|
||||||
|
|
||||||
totalConfirmers() {
|
totalConfirmers() {
|
||||||
return (
|
return (
|
||||||
this.numConfsNotVisible +
|
this.numConfsNotVisible +
|
||||||
@@ -853,8 +826,6 @@ export default class ClaimView extends Vue {
|
|||||||
});
|
});
|
||||||
if (giveResp.status === 200 && giveResp.data.data?.length > 0) {
|
if (giveResp.status === 200 && giveResp.data.data?.length > 0) {
|
||||||
this.detailsForGive = giveResp.data.data[0];
|
this.detailsForGive = giveResp.data.data[0];
|
||||||
// Extract offer information from the fulfills array
|
|
||||||
this.extractOfferFulfillment();
|
|
||||||
} else {
|
} else {
|
||||||
await this.$logError(
|
await this.$logError(
|
||||||
"Error getting detailed give info: " + JSON.stringify(giveResp),
|
"Error getting detailed give info: " + JSON.stringify(giveResp),
|
||||||
@@ -1131,21 +1102,16 @@ export default class ClaimView extends Vue {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async copyTextToClipboard(name: string, text: string) {
|
copyToClipboard(name: string, text: string) {
|
||||||
try {
|
useClipboard()
|
||||||
await copyToClipboard(text);
|
.copy(text)
|
||||||
this.notify.copied(name || "That");
|
.then(() => {
|
||||||
} catch (error) {
|
this.notify.copied(name || "That");
|
||||||
this.$logAndConsole(
|
});
|
||||||
`Error copying ${name || "content"} to clipboard: ${error}`,
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
this.notify.error(`Failed to copy ${name || "content"} to clipboard.`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onClickShareClaim() {
|
onClickShareClaim() {
|
||||||
this.copyTextToClipboard("A link to this page", this.windowDeepLink);
|
this.copyToClipboard("A link to this page", this.windowDeepLink);
|
||||||
window.navigator.share({
|
window.navigator.share({
|
||||||
title: "Help Connect Me",
|
title: "Help Connect Me",
|
||||||
text: "I'm trying to find the people who recorded this. Can you help me?",
|
text: "I'm trying to find the people who recorded this. Can you help me?",
|
||||||
|
|||||||
@@ -96,50 +96,50 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Fullfills Links -->
|
<!-- Fullfills Links -->
|
||||||
<div class="mt-4">
|
|
||||||
<!-- fullfills links for a give -->
|
|
||||||
<div v-if="giveDetails?.fulfillsPlanHandleId">
|
|
||||||
<router-link
|
|
||||||
:to="
|
|
||||||
'/project/' +
|
|
||||||
encodeURIComponent(
|
|
||||||
giveDetails?.fulfillsPlanHandleId || '',
|
|
||||||
)
|
|
||||||
"
|
|
||||||
class="text-blue-500 mt-2 cursor-pointer"
|
|
||||||
>
|
|
||||||
This fulfills a bigger plan
|
|
||||||
<font-awesome
|
|
||||||
icon="arrow-up-right-from-square"
|
|
||||||
class="fa-fw"
|
|
||||||
/>
|
|
||||||
</router-link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Show offer fulfillment if this give fulfills an offer -->
|
<!-- fullfills links for a give -->
|
||||||
<div v-if="giveDetailsOfferFulfillment?.offerHandleId">
|
<div v-if="giveDetails?.fulfillsPlanHandleId" class="mt-2">
|
||||||
<!-- router-link to /claim/ only changes URL path -->
|
<router-link
|
||||||
<router-link
|
:to="
|
||||||
:to="
|
'/project/' +
|
||||||
'/claim/' +
|
encodeURIComponent(giveDetails?.fulfillsPlanHandleId || '')
|
||||||
encodeURIComponent(
|
"
|
||||||
giveDetailsOfferFulfillment.offerHandleId || '',
|
class="text-blue-500 mt-2 cursor-pointer"
|
||||||
)
|
>
|
||||||
"
|
This fulfills a bigger plan
|
||||||
class="text-blue-500 mt-2 cursor-pointer"
|
<font-awesome
|
||||||
>
|
icon="arrow-up-right-from-square"
|
||||||
This fulfills
|
class="fa-fw"
|
||||||
{{
|
/>
|
||||||
serverUtil.capitalizeAndInsertSpacesBeforeCapsWithAPrefix(
|
</router-link>
|
||||||
giveDetailsOfferFulfillment.offerType || "Offer",
|
</div>
|
||||||
)
|
<!-- if there's another, it's probably fulfilling an offer, too -->
|
||||||
}}
|
<div
|
||||||
<font-awesome
|
v-if="
|
||||||
icon="arrow-up-right-from-square"
|
giveDetails?.fulfillsType &&
|
||||||
class="fa-fw"
|
giveDetails?.fulfillsType !== 'PlanAction' &&
|
||||||
/>
|
giveDetails?.fulfillsHandleId
|
||||||
</router-link>
|
"
|
||||||
</div>
|
>
|
||||||
|
<!-- router-link to /claim/ only changes URL path -->
|
||||||
|
<router-link
|
||||||
|
:to="
|
||||||
|
'/claim/' +
|
||||||
|
encodeURIComponent(giveDetails?.fulfillsHandleId || '')
|
||||||
|
"
|
||||||
|
class="text-blue-500 mt-2 cursor-pointer"
|
||||||
|
>
|
||||||
|
This fulfills
|
||||||
|
{{
|
||||||
|
capitalizeAndInsertSpacesBeforeCapsWithAPrefix(
|
||||||
|
giveDetails?.fulfillsType || "",
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
<font-awesome
|
||||||
|
icon="arrow-up-right-from-square"
|
||||||
|
class="fa-fw"
|
||||||
|
/>
|
||||||
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -192,7 +192,7 @@
|
|||||||
<span v-if="!serverUtil.isEmptyOrHiddenDid(confirmerId)">
|
<span v-if="!serverUtil.isEmptyOrHiddenDid(confirmerId)">
|
||||||
<button
|
<button
|
||||||
@click="
|
@click="
|
||||||
copyTextToClipboard(
|
copyToClipboard(
|
||||||
'The DID of ' + confirmerId,
|
'The DID of ' + confirmerId,
|
||||||
confirmerId,
|
confirmerId,
|
||||||
)
|
)
|
||||||
@@ -238,7 +238,7 @@
|
|||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
@click="
|
@click="
|
||||||
copyTextToClipboard(
|
copyToClipboard(
|
||||||
'The DID of ' + confsVisibleTo,
|
'The DID of ' + confsVisibleTo,
|
||||||
confsVisibleTo,
|
confsVisibleTo,
|
||||||
)
|
)
|
||||||
@@ -309,9 +309,7 @@
|
|||||||
contacts can see more details:
|
contacts can see more details:
|
||||||
<a
|
<a
|
||||||
class="text-blue-500"
|
class="text-blue-500"
|
||||||
@click="
|
@click="copyToClipboard('A link to this page', windowLocation)"
|
||||||
copyTextToClipboard('A link to this page', windowLocation)
|
|
||||||
"
|
|
||||||
>click to copy this page info</a
|
>click to copy this page info</a
|
||||||
>
|
>
|
||||||
and see if they can make an introduction. Someone is connected to
|
and see if they can make an introduction. Someone is connected to
|
||||||
@@ -334,9 +332,7 @@
|
|||||||
If you'd like an introduction,
|
If you'd like an introduction,
|
||||||
<a
|
<a
|
||||||
class="text-blue-500"
|
class="text-blue-500"
|
||||||
@click="
|
@click="copyToClipboard('A link to this page', windowLocation)"
|
||||||
copyTextToClipboard('A link to this page', windowLocation)
|
|
||||||
"
|
|
||||||
>share this page with them and ask if they'll tell you more about
|
>share this page with them and ask if they'll tell you more about
|
||||||
about the participants.</a
|
about the participants.</a
|
||||||
>
|
>
|
||||||
@@ -364,7 +360,7 @@
|
|||||||
<span v-if="!serverUtil.isEmptyOrHiddenDid(visDid)">
|
<span v-if="!serverUtil.isEmptyOrHiddenDid(visDid)">
|
||||||
<button
|
<button
|
||||||
@click="
|
@click="
|
||||||
copyTextToClipboard('The DID of ' + visDid, visDid)
|
copyToClipboard('The DID of ' + visDid, visDid)
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<font-awesome
|
<font-awesome
|
||||||
@@ -437,7 +433,7 @@
|
|||||||
import * as yaml from "js-yaml";
|
import * as yaml from "js-yaml";
|
||||||
import * as R from "ramda";
|
import * as R from "ramda";
|
||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
import { copyToClipboard } from "../services/ClipboardService";
|
import { useClipboard } from "@vueuse/core";
|
||||||
import { RouteLocationNormalizedLoaded, Router } from "vue-router";
|
import { RouteLocationNormalizedLoaded, Router } from "vue-router";
|
||||||
import QuickNav from "../components/QuickNav.vue";
|
import QuickNav from "../components/QuickNav.vue";
|
||||||
import { NotificationIface } from "../constants/app";
|
import { NotificationIface } from "../constants/app";
|
||||||
@@ -497,11 +493,6 @@ export default class ConfirmGiftView extends Vue {
|
|||||||
confsVisibleErrorMessage = "";
|
confsVisibleErrorMessage = "";
|
||||||
confsVisibleToIdList: string[] = []; // list of DIDs that can see any confirmer
|
confsVisibleToIdList: string[] = []; // list of DIDs that can see any confirmer
|
||||||
giveDetails?: GiveSummaryRecord;
|
giveDetails?: GiveSummaryRecord;
|
||||||
// Additional offer information extracted from the fulfills array
|
|
||||||
giveDetailsOfferFulfillment: {
|
|
||||||
offerHandleId?: string;
|
|
||||||
offerType?: string;
|
|
||||||
} | null = null;
|
|
||||||
giverName = "";
|
giverName = "";
|
||||||
issuerName = "";
|
issuerName = "";
|
||||||
isLoading = false;
|
isLoading = false;
|
||||||
@@ -657,8 +648,6 @@ export default class ConfirmGiftView extends Vue {
|
|||||||
|
|
||||||
if (resp.status === 200) {
|
if (resp.status === 200) {
|
||||||
this.giveDetails = resp.data.data[0];
|
this.giveDetails = resp.data.data[0];
|
||||||
// Extract offer information from the fulfills array
|
|
||||||
this.extractOfferFulfillment();
|
|
||||||
} else {
|
} else {
|
||||||
throw new Error("Error getting detailed give info: " + resp.status);
|
throw new Error("Error getting detailed give info: " + resp.status);
|
||||||
}
|
}
|
||||||
@@ -718,15 +707,6 @@ export default class ConfirmGiftView extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Extract offer fulfillment information from the fulfills array
|
|
||||||
*/
|
|
||||||
private extractOfferFulfillment() {
|
|
||||||
this.giveDetailsOfferFulfillment = libsUtil.extractOfferFulfillment(
|
|
||||||
this.giveDetails?.fullClaim?.fulfills
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetches confirmer information for the claim
|
* Fetches confirmer information for the claim
|
||||||
*/
|
*/
|
||||||
@@ -783,21 +763,16 @@ export default class ConfirmGiftView extends Vue {
|
|||||||
* @param description - Description of copied content
|
* @param description - Description of copied content
|
||||||
* @param text - Text to copy
|
* @param text - Text to copy
|
||||||
*/
|
*/
|
||||||
async copyTextToClipboard(description: string, text: string): Promise<void> {
|
copyToClipboard(description: string, text: string): void {
|
||||||
try {
|
useClipboard()
|
||||||
await copyToClipboard(text);
|
.copy(text)
|
||||||
this.notify.toast(
|
.then(() => {
|
||||||
NOTIFY_COPIED_TO_CLIPBOARD.title,
|
this.notify.toast(
|
||||||
NOTIFY_COPIED_TO_CLIPBOARD.message(description),
|
NOTIFY_COPIED_TO_CLIPBOARD.title,
|
||||||
TIMEOUTS.SHORT,
|
NOTIFY_COPIED_TO_CLIPBOARD.message(description),
|
||||||
);
|
TIMEOUTS.SHORT,
|
||||||
} catch (error) {
|
);
|
||||||
this.$logAndConsole(
|
});
|
||||||
`Error copying ${description} to clipboard: ${error}`,
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
this.notify.error(`Failed to copy ${description} to clipboard.`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -874,12 +849,33 @@ export default class ConfirmGiftView extends Vue {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats type string for display by adding spaces before capitals
|
||||||
|
* Optionally adds a prefix
|
||||||
|
*
|
||||||
|
* @param text - Text to format
|
||||||
|
* @param prefix - Optional prefix to add
|
||||||
|
* @returns Formatted string
|
||||||
|
*/
|
||||||
|
capitalizeAndInsertSpacesBeforeCapsWithAPrefix(text: string): string {
|
||||||
|
const word = this.capitalizeAndInsertSpacesBeforeCaps(text);
|
||||||
|
if (word) {
|
||||||
|
// if the word starts with a vowel, use "an" instead of "a"
|
||||||
|
const firstLetter = word[0].toLowerCase();
|
||||||
|
const vowels = ["a", "e", "i", "o", "u"];
|
||||||
|
const particle = vowels.includes(firstLetter) ? "an" : "a";
|
||||||
|
return particle + " " + word;
|
||||||
|
} else {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initiates sharing of claim information
|
* Initiates sharing of claim information
|
||||||
* Handles share functionality based on platform capabilities
|
* Handles share functionality based on platform capabilities
|
||||||
*/
|
*/
|
||||||
async onClickShareClaim(): Promise<void> {
|
async onClickShareClaim(): Promise<void> {
|
||||||
this.copyTextToClipboard("A link to this page", this.windowLocation);
|
this.copyToClipboard("A link to this page", this.windowLocation);
|
||||||
window.navigator.share({
|
window.navigator.share({
|
||||||
title: "Help Connect Me",
|
title: "Help Connect Me",
|
||||||
text: "I'm trying to find the full details of this claim. Can you help me?",
|
text: "I'm trying to find the full details of this claim. Can you help me?",
|
||||||
@@ -898,5 +894,11 @@ export default class ConfirmGiftView extends Vue {
|
|||||||
this.veriClaim = serverUtil.BLANK_GENERIC_SERVER_RECORD;
|
this.veriClaim = serverUtil.BLANK_GENERIC_SERVER_RECORD;
|
||||||
this.veriClaimDump = "";
|
this.veriClaimDump = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
capitalizeAndInsertSpacesBeforeCaps(text: string) {
|
||||||
|
return !text
|
||||||
|
? ""
|
||||||
|
: text[0].toUpperCase() + text.substr(1).replace(/([A-Z])/g, " $1");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ import { Buffer } from "buffer/";
|
|||||||
import QRCodeVue3 from "qr-code-generator-vue3";
|
import QRCodeVue3 from "qr-code-generator-vue3";
|
||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
import { Router } from "vue-router";
|
import { Router } from "vue-router";
|
||||||
import { copyToClipboard } from "../services/ClipboardService";
|
import { useClipboard } from "@vueuse/core";
|
||||||
|
|
||||||
import { logger } from "../utils/logger";
|
import { logger } from "../utils/logger";
|
||||||
import { QRScannerFactory } from "../services/QRScanner/QRScannerFactory";
|
import { QRScannerFactory } from "../services/QRScanner/QRScannerFactory";
|
||||||
@@ -144,7 +144,6 @@ import {
|
|||||||
QR_TIMEOUT_LONG,
|
QR_TIMEOUT_LONG,
|
||||||
} from "@/constants/notifications";
|
} from "@/constants/notifications";
|
||||||
import { createNotifyHelpers, NotifyFunction } from "../utils/notify";
|
import { createNotifyHelpers, NotifyFunction } from "../utils/notify";
|
||||||
import { showSeedPhraseReminder } from "@/utils/seedPhraseReminder";
|
|
||||||
|
|
||||||
interface QRScanResult {
|
interface QRScanResult {
|
||||||
rawValue?: string;
|
rawValue?: string;
|
||||||
@@ -196,7 +195,7 @@ export default class ContactQRScanFull extends Vue {
|
|||||||
$router!: Router;
|
$router!: Router;
|
||||||
|
|
||||||
// Notification helper system
|
// Notification helper system
|
||||||
private notify!: ReturnType<typeof createNotifyHelpers>;
|
private notify = createNotifyHelpers(this.$notify);
|
||||||
|
|
||||||
isScanning = false;
|
isScanning = false;
|
||||||
error: string | null = null;
|
error: string | null = null;
|
||||||
@@ -264,9 +263,6 @@ export default class ContactQRScanFull extends Vue {
|
|||||||
* Loads user settings and generates QR code for contact sharing
|
* Loads user settings and generates QR code for contact sharing
|
||||||
*/
|
*/
|
||||||
async created() {
|
async created() {
|
||||||
// Initialize notification helper system
|
|
||||||
this.notify = createNotifyHelpers(this.$notify);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const settings = await this.$accountSettings();
|
const settings = await this.$accountSettings();
|
||||||
this.activeDid = settings.activeDid || "";
|
this.activeDid = settings.activeDid || "";
|
||||||
@@ -626,15 +622,6 @@ export default class ContactQRScanFull extends Vue {
|
|||||||
*/
|
*/
|
||||||
async handleBack() {
|
async handleBack() {
|
||||||
await this.cleanupScanner();
|
await this.cleanupScanner();
|
||||||
|
|
||||||
// Show seed phrase backup reminder if needed
|
|
||||||
try {
|
|
||||||
const settings = await this.$accountSettings();
|
|
||||||
showSeedPhraseReminder(!!settings.hasBackedUpSeed, this.$notify);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error("Error checking seed backup status:", error);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$router.back();
|
this.$router.back();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -649,51 +636,36 @@ export default class ContactQRScanFull extends Vue {
|
|||||||
* Copies contact URL to clipboard for sharing
|
* Copies contact URL to clipboard for sharing
|
||||||
*/
|
*/
|
||||||
async onCopyUrlToClipboard() {
|
async onCopyUrlToClipboard() {
|
||||||
try {
|
const account = (await libsUtil.retrieveFullyDecryptedAccount(
|
||||||
const account = (await libsUtil.retrieveFullyDecryptedAccount(
|
this.activeDid,
|
||||||
this.activeDid,
|
)) as Account;
|
||||||
)) as Account;
|
const jwtUrl = await generateEndorserJwtUrlForAccount(
|
||||||
const jwtUrl = await generateEndorserJwtUrlForAccount(
|
account,
|
||||||
account,
|
this.isRegistered,
|
||||||
this.isRegistered,
|
this.givenName,
|
||||||
this.givenName,
|
this.profileImageUrl,
|
||||||
this.profileImageUrl,
|
true,
|
||||||
true,
|
);
|
||||||
);
|
useClipboard()
|
||||||
|
.copy(jwtUrl)
|
||||||
// Use the platform-specific ClipboardService for reliable iOS support
|
.then(() => {
|
||||||
await copyToClipboard(jwtUrl);
|
this.notify.toast(
|
||||||
|
NOTIFY_QR_URL_COPIED.title,
|
||||||
this.notify.toast(
|
NOTIFY_QR_URL_COPIED.message,
|
||||||
NOTIFY_QR_URL_COPIED.title,
|
QR_TIMEOUT_MEDIUM,
|
||||||
NOTIFY_QR_URL_COPIED.message,
|
);
|
||||||
QR_TIMEOUT_MEDIUM,
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error("Error copying URL to clipboard:", {
|
|
||||||
error: error instanceof Error ? error.message : String(error),
|
|
||||||
stack: error instanceof Error ? error.stack : undefined,
|
|
||||||
});
|
});
|
||||||
this.notify.error("Failed to copy URL to clipboard.");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Copies DID to clipboard for manual sharing
|
* Copies DID to clipboard for manual sharing
|
||||||
*/
|
*/
|
||||||
async onCopyDidToClipboard() {
|
onCopyDidToClipboard() {
|
||||||
try {
|
useClipboard()
|
||||||
// Use the platform-specific ClipboardService for reliable iOS support
|
.copy(this.activeDid)
|
||||||
await copyToClipboard(this.activeDid);
|
.then(() => {
|
||||||
|
this.notify.info(NOTIFY_QR_DID_COPIED.message, QR_TIMEOUT_LONG);
|
||||||
this.notify.info(NOTIFY_QR_DID_COPIED.message, QR_TIMEOUT_LONG);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error("Error copying DID to clipboard:", {
|
|
||||||
error: error instanceof Error ? error.message : String(error),
|
|
||||||
stack: error instanceof Error ? error.stack : undefined,
|
|
||||||
});
|
});
|
||||||
this.notify.error("Failed to copy DID to clipboard.");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -140,7 +140,6 @@ import { AxiosError } from "axios";
|
|||||||
import { Buffer } from "buffer/";
|
import { Buffer } from "buffer/";
|
||||||
import QRCodeVue3 from "qr-code-generator-vue3";
|
import QRCodeVue3 from "qr-code-generator-vue3";
|
||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
import { copyToClipboard } from "../services/ClipboardService";
|
|
||||||
|
|
||||||
import { QrcodeStream } from "vue-qrcode-reader";
|
import { QrcodeStream } from "vue-qrcode-reader";
|
||||||
|
|
||||||
@@ -164,7 +163,6 @@ import { QRScannerFactory } from "@/services/QRScanner/QRScannerFactory";
|
|||||||
import { CameraState } from "@/services/QRScanner/types";
|
import { CameraState } from "@/services/QRScanner/types";
|
||||||
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
||||||
import { createNotifyHelpers } from "@/utils/notify";
|
import { createNotifyHelpers } from "@/utils/notify";
|
||||||
import { showSeedPhraseReminder } from "@/utils/seedPhraseReminder";
|
|
||||||
import {
|
import {
|
||||||
NOTIFY_QR_INITIALIZATION_ERROR,
|
NOTIFY_QR_INITIALIZATION_ERROR,
|
||||||
NOTIFY_QR_CAMERA_IN_USE,
|
NOTIFY_QR_CAMERA_IN_USE,
|
||||||
@@ -321,15 +319,6 @@ export default class ContactQRScanShow extends Vue {
|
|||||||
|
|
||||||
async handleBack(): Promise<void> {
|
async handleBack(): Promise<void> {
|
||||||
await this.cleanupScanner();
|
await this.cleanupScanner();
|
||||||
|
|
||||||
// Show seed phrase backup reminder if needed
|
|
||||||
try {
|
|
||||||
const settings = await this.$accountSettings();
|
|
||||||
showSeedPhraseReminder(!!settings.hasBackedUpSeed, this.$notify);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error("Error checking seed backup status:", error);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$router.back();
|
this.$router.back();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -629,6 +618,7 @@ export default class ContactQRScanShow extends Vue {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Copy the URL to clipboard
|
// Copy the URL to clipboard
|
||||||
|
const { copyToClipboard } = await import("../services/ClipboardService");
|
||||||
await copyToClipboard(jwtUrl);
|
await copyToClipboard(jwtUrl);
|
||||||
this.notify.toast(
|
this.notify.toast(
|
||||||
NOTIFY_QR_URL_COPIED.title,
|
NOTIFY_QR_URL_COPIED.title,
|
||||||
@@ -647,6 +637,7 @@ export default class ContactQRScanShow extends Vue {
|
|||||||
async onCopyDidToClipboard() {
|
async onCopyDidToClipboard() {
|
||||||
//this.onScanDetect([{ rawValue: this.qrValue }]); // good for testing
|
//this.onScanDetect([{ rawValue: this.qrValue }]); // good for testing
|
||||||
try {
|
try {
|
||||||
|
const { copyToClipboard } = await import("../services/ClipboardService");
|
||||||
await copyToClipboard(this.activeDid);
|
await copyToClipboard(this.activeDid);
|
||||||
this.notify.info(NOTIFY_QR_DID_COPIED.message, QR_TIMEOUT_LONG);
|
this.notify.info(NOTIFY_QR_DID_COPIED.message, QR_TIMEOUT_LONG);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -747,17 +738,24 @@ export default class ContactQRScanShow extends Vue {
|
|||||||
!contact.registered
|
!contact.registered
|
||||||
) {
|
) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.$notify(
|
this.notify.confirm(
|
||||||
|
"Do you want to register them?",
|
||||||
{
|
{
|
||||||
group: "modal",
|
|
||||||
type: "confirm",
|
|
||||||
title: "Register",
|
|
||||||
text: "Do you want to register them?",
|
|
||||||
onCancel: async (stopAsking?: boolean) => {
|
onCancel: async (stopAsking?: boolean) => {
|
||||||
await this.handleRegistrationPromptResponse(stopAsking);
|
if (stopAsking) {
|
||||||
|
await this.$updateSettings({
|
||||||
|
hideRegisterPromptOnNewContact: stopAsking,
|
||||||
|
});
|
||||||
|
this.hideRegisterPromptOnNewContact = stopAsking;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onNo: async (stopAsking?: boolean) => {
|
onNo: async (stopAsking?: boolean) => {
|
||||||
await this.handleRegistrationPromptResponse(stopAsking);
|
if (stopAsking) {
|
||||||
|
await this.$updateSettings({
|
||||||
|
hideRegisterPromptOnNewContact: stopAsking,
|
||||||
|
});
|
||||||
|
this.hideRegisterPromptOnNewContact = stopAsking;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onYes: async () => {
|
onYes: async () => {
|
||||||
await this.register(contact);
|
await this.register(contact);
|
||||||
@@ -887,17 +885,6 @@ export default class ContactQRScanShow extends Vue {
|
|||||||
videoElement.style.transform = shouldMirror ? "scaleX(-1)" : "none";
|
videoElement.style.transform = shouldMirror ? "scaleX(-1)" : "none";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async handleRegistrationPromptResponse(
|
|
||||||
stopAsking?: boolean,
|
|
||||||
): Promise<void> {
|
|
||||||
if (stopAsking) {
|
|
||||||
await this.$saveSettings({
|
|
||||||
hideRegisterPromptOnNewContact: stopAsking,
|
|
||||||
});
|
|
||||||
this.hideRegisterPromptOnNewContact = stopAsking;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -71,22 +71,22 @@
|
|||||||
contactFromDid?.seesMe && contactFromDid.did !== activeDid
|
contactFromDid?.seesMe && contactFromDid.did !== activeDid
|
||||||
"
|
"
|
||||||
class="text-sm uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
|
class="text-sm uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
|
||||||
title="They can see your activity"
|
title="They can see you"
|
||||||
@click="confirmSetVisibility(contactFromDid, false)"
|
@click="confirmSetVisibility(contactFromDid, false)"
|
||||||
>
|
>
|
||||||
<font-awesome icon="arrow-up" class="fa-fw" />
|
|
||||||
<font-awesome icon="eye" class="fa-fw" />
|
<font-awesome icon="eye" class="fa-fw" />
|
||||||
|
<font-awesome icon="arrow-up" class="fa-fw" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-else-if="
|
v-else-if="
|
||||||
!contactFromDid?.seesMe && contactFromDid?.did !== activeDid
|
!contactFromDid?.seesMe && contactFromDid?.did !== activeDid
|
||||||
"
|
"
|
||||||
class="text-sm uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
|
class="text-sm uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
|
||||||
title="They cannot see your activity"
|
title="They cannot see you"
|
||||||
@click="confirmSetVisibility(contactFromDid, true)"
|
@click="confirmSetVisibility(contactFromDid, true)"
|
||||||
>
|
>
|
||||||
<font-awesome icon="arrow-up" class="fa-fw" />
|
|
||||||
<font-awesome icon="eye-slash" class="fa-fw" />
|
<font-awesome icon="eye-slash" class="fa-fw" />
|
||||||
|
<font-awesome icon="arrow-up" class="fa-fw" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
@@ -95,11 +95,11 @@
|
|||||||
contactFromDid.did !== activeDid
|
contactFromDid.did !== activeDid
|
||||||
"
|
"
|
||||||
class="text-sm uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
|
class="text-sm uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
|
||||||
title="You watch their activity"
|
title="I view their content"
|
||||||
@click="confirmViewContent(contactFromDid, false)"
|
@click="confirmViewContent(contactFromDid, false)"
|
||||||
>
|
>
|
||||||
<font-awesome icon="arrow-down" class="fa-fw" />
|
|
||||||
<font-awesome icon="eye" class="fa-fw" />
|
<font-awesome icon="eye" class="fa-fw" />
|
||||||
|
<font-awesome icon="arrow-down" class="fa-fw" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-else-if="
|
v-else-if="
|
||||||
@@ -107,11 +107,11 @@
|
|||||||
contactFromDid?.did !== activeDid
|
contactFromDid?.did !== activeDid
|
||||||
"
|
"
|
||||||
class="text-sm uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
|
class="text-sm uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
|
||||||
title="You do not watch their activity"
|
title="I do not view their content"
|
||||||
@click="confirmViewContent(contactFromDid, true)"
|
@click="confirmViewContent(contactFromDid, true)"
|
||||||
>
|
>
|
||||||
<font-awesome icon="arrow-down" class="fa-fw" />
|
|
||||||
<font-awesome icon="eye-slash" class="fa-fw" />
|
<font-awesome icon="eye-slash" class="fa-fw" />
|
||||||
|
<font-awesome icon="arrow-down" class="fa-fw" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -1003,7 +1003,7 @@
|
|||||||
<h2>Exported Data</h2>
|
<h2>Exported Data</h2>
|
||||||
<span
|
<span
|
||||||
class="text-blue-500 cursor-pointer hover:text-blue-700"
|
class="text-blue-500 cursor-pointer hover:text-blue-700"
|
||||||
@click="copyExportedDataToClipboard"
|
@click="copyToClipboard"
|
||||||
>
|
>
|
||||||
Copy to Clipboard
|
Copy to Clipboard
|
||||||
</span>
|
</span>
|
||||||
@@ -1014,7 +1014,7 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
import { copyToClipboard } from "../services/ClipboardService";
|
import { useClipboard } from "@vueuse/core";
|
||||||
import { Router } from "vue-router";
|
import { Router } from "vue-router";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -1072,6 +1072,8 @@ export default class DatabaseMigration extends Vue {
|
|||||||
private exportedData: Record<string, any> | null = null;
|
private exportedData: Record<string, any> | null = null;
|
||||||
private successMessage = "";
|
private successMessage = "";
|
||||||
|
|
||||||
|
useClipboard = useClipboard;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Computed property to get the display name for a setting
|
* Computed property to get the display name for a setting
|
||||||
* Handles both live comparison data and exported JSON format
|
* Handles both live comparison data and exported JSON format
|
||||||
@@ -1131,11 +1133,13 @@ export default class DatabaseMigration extends Vue {
|
|||||||
/**
|
/**
|
||||||
* Copies exported data to clipboard and shows success message
|
* Copies exported data to clipboard and shows success message
|
||||||
*/
|
*/
|
||||||
async copyExportedDataToClipboard(): Promise<void> {
|
async copyToClipboard(): Promise<void> {
|
||||||
if (!this.exportedData) return;
|
if (!this.exportedData) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await copyToClipboard(JSON.stringify(this.exportedData, null, 2));
|
await this.useClipboard().copy(
|
||||||
|
JSON.stringify(this.exportedData, null, 2),
|
||||||
|
);
|
||||||
// Use global window object properly
|
// Use global window object properly
|
||||||
if (typeof window !== "undefined") {
|
if (typeof window !== "undefined") {
|
||||||
window.alert("Copied to clipboard!");
|
window.alert("Copied to clipboard!");
|
||||||
|
|||||||
@@ -51,6 +51,33 @@
|
|||||||
<!-- Secondary Tabs -->
|
<!-- Secondary Tabs -->
|
||||||
<div class="text-center text-slate-500 border-b border-slate-300">
|
<div class="text-center text-slate-500 border-b border-slate-300">
|
||||||
<ul class="flex flex-wrap justify-center gap-4 -mb-px">
|
<ul class="flex flex-wrap justify-center gap-4 -mb-px">
|
||||||
|
<li v-if="isProjectsActive">
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
:class="computedStarredTabStyleClassNames()"
|
||||||
|
@click="
|
||||||
|
projects = [];
|
||||||
|
userProfiles = [];
|
||||||
|
isStarredActive = true;
|
||||||
|
isLocalActive = false;
|
||||||
|
isMappedActive = false;
|
||||||
|
isAnywhereActive = false;
|
||||||
|
isSearchVisible = false;
|
||||||
|
tempSearchBox = null;
|
||||||
|
searchStarred();
|
||||||
|
"
|
||||||
|
>
|
||||||
|
Starred
|
||||||
|
<!-- restore when the links don't jump around for different numbers
|
||||||
|
<span
|
||||||
|
class="font-semibold text-sm bg-slate-200 px-1.5 py-0.5 rounded-md"
|
||||||
|
v-if="isLocalActive"
|
||||||
|
>
|
||||||
|
{{ localCount > -1 ? localCount : "?" }}
|
||||||
|
</span>
|
||||||
|
-->
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
@@ -58,9 +85,11 @@
|
|||||||
@click="
|
@click="
|
||||||
projects = [];
|
projects = [];
|
||||||
userProfiles = [];
|
userProfiles = [];
|
||||||
|
isStarredActive = false;
|
||||||
isLocalActive = true;
|
isLocalActive = true;
|
||||||
isMappedActive = false;
|
isMappedActive = false;
|
||||||
isAnywhereActive = false;
|
isAnywhereActive = false;
|
||||||
|
isStarredActive = false;
|
||||||
isSearchVisible = true;
|
isSearchVisible = true;
|
||||||
tempSearchBox = null;
|
tempSearchBox = null;
|
||||||
searchLocal();
|
searchLocal();
|
||||||
@@ -84,9 +113,11 @@
|
|||||||
@click="
|
@click="
|
||||||
projects = [];
|
projects = [];
|
||||||
userProfiles = [];
|
userProfiles = [];
|
||||||
|
isStarredActive = false;
|
||||||
isLocalActive = false;
|
isLocalActive = false;
|
||||||
isMappedActive = true;
|
isMappedActive = true;
|
||||||
isAnywhereActive = false;
|
isAnywhereActive = false;
|
||||||
|
isStarredActive = false;
|
||||||
isSearchVisible = false;
|
isSearchVisible = false;
|
||||||
searchTerms = '';
|
searchTerms = '';
|
||||||
tempSearchBox = null;
|
tempSearchBox = null;
|
||||||
@@ -103,9 +134,11 @@
|
|||||||
@click="
|
@click="
|
||||||
projects = [];
|
projects = [];
|
||||||
userProfiles = [];
|
userProfiles = [];
|
||||||
|
isStarredActive = false;
|
||||||
isLocalActive = false;
|
isLocalActive = false;
|
||||||
isMappedActive = false;
|
isMappedActive = false;
|
||||||
isAnywhereActive = true;
|
isAnywhereActive = true;
|
||||||
|
isStarredActive = false;
|
||||||
isSearchVisible = true;
|
isSearchVisible = true;
|
||||||
tempSearchBox = null;
|
tempSearchBox = null;
|
||||||
searchAll();
|
searchAll();
|
||||||
@@ -201,6 +234,15 @@
|
|||||||
>No {{ isProjectsActive ? "projects" : "people" }} were found with
|
>No {{ isProjectsActive ? "projects" : "people" }} were found with
|
||||||
that search.</span
|
that search.</span
|
||||||
>
|
>
|
||||||
|
<span v-else-if="isStarredActive">
|
||||||
|
<p>
|
||||||
|
You have no starred projects. Star some projects to see them here.
|
||||||
|
</p>
|
||||||
|
<p class="mt-4">
|
||||||
|
When you star projects, you will get a notice on the front page when
|
||||||
|
they change.
|
||||||
|
</p>
|
||||||
|
</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -383,9 +425,12 @@ export default class DiscoverView extends Vue {
|
|||||||
allMyDids: Array<string> = [];
|
allMyDids: Array<string> = [];
|
||||||
apiServer = "";
|
apiServer = "";
|
||||||
isLoading = false;
|
isLoading = false;
|
||||||
|
|
||||||
isLocalActive = false;
|
isLocalActive = false;
|
||||||
isMappedActive = false;
|
isMappedActive = false;
|
||||||
isAnywhereActive = true;
|
isAnywhereActive = true;
|
||||||
|
isStarredActive = false;
|
||||||
|
|
||||||
isProjectsActive = true;
|
isProjectsActive = true;
|
||||||
isPeopleActive = false;
|
isPeopleActive = false;
|
||||||
isSearchVisible = true;
|
isSearchVisible = true;
|
||||||
@@ -470,6 +515,8 @@ export default class DiscoverView extends Vue {
|
|||||||
leafletObject: L.Map;
|
leafletObject: L.Map;
|
||||||
};
|
};
|
||||||
this.requestTiles(mapRef.leafletObject); // not ideal because I found this from experimentation, not documentation
|
this.requestTiles(mapRef.leafletObject); // not ideal because I found this from experimentation, not documentation
|
||||||
|
} else if (this.isStarredActive) {
|
||||||
|
await this.searchStarred();
|
||||||
} else {
|
} else {
|
||||||
await this.searchAll();
|
await this.searchAll();
|
||||||
}
|
}
|
||||||
@@ -540,6 +587,60 @@ export default class DiscoverView extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async searchStarred() {
|
||||||
|
this.resetCounts();
|
||||||
|
|
||||||
|
// Clear any previous results
|
||||||
|
this.projects = [];
|
||||||
|
this.userProfiles = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.isLoading = true;
|
||||||
|
|
||||||
|
// Get starred project IDs from settings
|
||||||
|
const settings = await this.$accountSettings();
|
||||||
|
|
||||||
|
const starredIds = settings.starredPlanHandleIds || [];
|
||||||
|
if (starredIds.length === 0) {
|
||||||
|
// No starred projects
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This could be optimized to only pull those not already in the cache (endorserServer.ts)
|
||||||
|
|
||||||
|
const planHandleIdsJson = JSON.stringify(starredIds);
|
||||||
|
const endpoint =
|
||||||
|
this.apiServer +
|
||||||
|
"/api/v2/report/plans?planHandleIds=" +
|
||||||
|
encodeURIComponent(planHandleIdsJson);
|
||||||
|
const response = await this.axios.get(endpoint, {
|
||||||
|
headers: await getHeaders(this.activeDid),
|
||||||
|
});
|
||||||
|
if (response.status !== 200) {
|
||||||
|
this.notify.error("Failed to load starred projects", TIMEOUTS.SHORT);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const starredPlans: PlanData[] = response.data.data;
|
||||||
|
if (response.data.hitLimit) {
|
||||||
|
// someday we'll have to let them incrementally load the rest
|
||||||
|
this.notify.warning(
|
||||||
|
"Beware: you have so many starred projects that we cannot load them all.",
|
||||||
|
TIMEOUTS.SHORT,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.projects = starredPlans;
|
||||||
|
} catch (error: unknown) {
|
||||||
|
logger.error("Error loading starred projects:", error);
|
||||||
|
this.notify.error(
|
||||||
|
"Failed to load starred projects. Please try again.",
|
||||||
|
TIMEOUTS.LONG,
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
this.isLoading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async searchLocal(beforeId?: string) {
|
public async searchLocal(beforeId?: string) {
|
||||||
this.resetCounts();
|
this.resetCounts();
|
||||||
|
|
||||||
@@ -633,9 +734,12 @@ export default class DiscoverView extends Vue {
|
|||||||
const latestProject = this.projects[this.projects.length - 1];
|
const latestProject = this.projects[this.projects.length - 1];
|
||||||
if (this.isLocalActive || this.isMappedActive) {
|
if (this.isLocalActive || this.isMappedActive) {
|
||||||
this.searchLocal(latestProject.rowId);
|
this.searchLocal(latestProject.rowId);
|
||||||
|
} else if (this.isStarredActive) {
|
||||||
|
this.searchStarred();
|
||||||
} else if (this.isAnywhereActive) {
|
} else if (this.isAnywhereActive) {
|
||||||
this.searchAll(latestProject.rowId);
|
this.searchAll(latestProject.rowId);
|
||||||
}
|
}
|
||||||
|
// Note: Starred tab doesn't support pagination since we load all starred projects at once
|
||||||
} else if (this.isPeopleActive && this.userProfiles.length > 0) {
|
} else if (this.isPeopleActive && this.userProfiles.length > 0) {
|
||||||
const latestProfile = this.userProfiles[this.userProfiles.length - 1];
|
const latestProfile = this.userProfiles[this.userProfiles.length - 1];
|
||||||
if (this.isLocalActive || this.isMappedActive) {
|
if (this.isLocalActive || this.isMappedActive) {
|
||||||
@@ -775,6 +879,24 @@ export default class DiscoverView extends Vue {
|
|||||||
this.$router.push(route);
|
this.$router.push(route);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public computedStarredTabStyleClassNames() {
|
||||||
|
return {
|
||||||
|
"inline-block": true,
|
||||||
|
"py-3": true,
|
||||||
|
"rounded-t-lg": true,
|
||||||
|
"border-b-2": true,
|
||||||
|
|
||||||
|
active: this.isStarredActive,
|
||||||
|
"text-black": this.isStarredActive,
|
||||||
|
"border-black": this.isStarredActive,
|
||||||
|
"font-semibold": this.isStarredActive,
|
||||||
|
|
||||||
|
"text-blue-600": !this.isStarredActive,
|
||||||
|
"border-transparent": !this.isStarredActive,
|
||||||
|
"hover:border-slate-400": !this.isStarredActive,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
public computedLocalTabStyleClassNames() {
|
public computedLocalTabStyleClassNames() {
|
||||||
return {
|
return {
|
||||||
"inline-block": true,
|
"inline-block": true,
|
||||||
|
|||||||
@@ -280,7 +280,6 @@ import { logger } from "../utils/logger";
|
|||||||
import { Contact } from "@/db/tables/contacts";
|
import { Contact } from "@/db/tables/contacts";
|
||||||
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
||||||
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
|
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
|
||||||
import { showSeedPhraseReminder } from "@/utils/seedPhraseReminder";
|
|
||||||
import {
|
import {
|
||||||
NOTIFY_GIFTED_DETAILS_RETRIEVAL_ERROR,
|
NOTIFY_GIFTED_DETAILS_RETRIEVAL_ERROR,
|
||||||
NOTIFY_GIFTED_DETAILS_DELETE_IMAGE_CONFIRM,
|
NOTIFY_GIFTED_DETAILS_DELETE_IMAGE_CONFIRM,
|
||||||
@@ -771,15 +770,6 @@ export default class GiftedDetails extends Vue {
|
|||||||
NOTIFY_GIFTED_DETAILS_GIFT_RECORDED.message,
|
NOTIFY_GIFTED_DETAILS_GIFT_RECORDED.message,
|
||||||
TIMEOUTS.SHORT,
|
TIMEOUTS.SHORT,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Show seed phrase backup reminder if needed
|
|
||||||
try {
|
|
||||||
const settings = await this.$accountSettings();
|
|
||||||
showSeedPhraseReminder(!!settings.hasBackedUpSeed, this.$notify);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error("Error checking seed backup status:", error);
|
|
||||||
}
|
|
||||||
|
|
||||||
localStorage.removeItem("imageUrl");
|
localStorage.removeItem("imageUrl");
|
||||||
if (this.destinationPathAfter) {
|
if (this.destinationPathAfter) {
|
||||||
(this.$router as Router).push({ path: this.destinationPathAfter });
|
(this.$router as Router).push({ path: this.destinationPathAfter });
|
||||||
|
|||||||
@@ -319,9 +319,8 @@
|
|||||||
<ul class="list-disc list-outside ml-4">
|
<ul class="list-disc list-outside ml-4">
|
||||||
<li>
|
<li>
|
||||||
Go to Your Identity <font-awesome icon="circle-user" class="fa-fw" /> page,
|
Go to Your Identity <font-awesome icon="circle-user" class="fa-fw" /> page,
|
||||||
click Advanced, and follow the instructions to "Import Contacts".
|
click Advanced, and follow the instructions for the Contacts & Settings Database "Import".
|
||||||
(There is currently no way to import other settings, so you'll have to recreate
|
Beware that this will erase your existing contact & settings.
|
||||||
by hand your search area, filters, etc.)
|
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@@ -337,18 +336,14 @@
|
|||||||
|
|
||||||
<h2 class="text-xl font-semibold">How do I erase my data from my device?</h2>
|
<h2 class="text-xl font-semibold">How do I erase my data from my device?</h2>
|
||||||
<p>
|
<p>
|
||||||
Before doing this, you should back up your data with the instructions above.
|
Before doing this, you may want to back up your data with the instructions above.
|
||||||
Note that this does not erase data sent to our servers (see contact info below)
|
|
||||||
</p>
|
</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li class="list-disc list-outside ml-4">
|
<li class="list-disc list-outside ml-4">
|
||||||
Mobile
|
Mobile
|
||||||
<ul>
|
<ul>
|
||||||
<li class="list-disc list-outside ml-4">
|
<li class="list-disc list-outside ml-4">
|
||||||
App Store app: hold down on the icon, then uninstall it
|
Home Screen: hold down on the icon, and choose to delete it
|
||||||
</li>
|
|
||||||
<li class="list-disc list-outside ml-4">
|
|
||||||
Home Screen PWA: hold down on the icon, and delete it
|
|
||||||
</li>
|
</li>
|
||||||
<li class="list-disc list-outside ml-4">
|
<li class="list-disc list-outside ml-4">
|
||||||
Chrome: Settings -> Privacy and Security -> Clear Browsing Data
|
Chrome: Settings -> Privacy and Security -> Clear Browsing Data
|
||||||
@@ -420,6 +415,15 @@
|
|||||||
different page.
|
different page.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<h2 class="text-xl font-semibold">
|
||||||
|
Where do I get help with notifications?
|
||||||
|
</h2>
|
||||||
|
<p>
|
||||||
|
<router-link class="text-blue-500" to="/help-notifications"
|
||||||
|
>Here.</router-link
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
|
||||||
<h2 class="text-xl font-semibold">
|
<h2 class="text-xl font-semibold">
|
||||||
This app is misbehaving, like showing me a blank screen or failing to show my personal data.
|
This app is misbehaving, like showing me a blank screen or failing to show my personal data.
|
||||||
What can I do?
|
What can I do?
|
||||||
@@ -430,13 +434,10 @@
|
|||||||
</p>
|
</p>
|
||||||
<ul class="list-disc list-outside ml-4">
|
<ul class="list-disc list-outside ml-4">
|
||||||
<li>
|
<li>
|
||||||
For mobile apps, make sure you're connected to the internet.
|
Drag down on the screen to refresh it; do that multiple times, because
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
For PWAs, drag down on the screen to refresh it; do that multiple times, because
|
|
||||||
it sometimes takes multiple tries for the app to refresh to the latest version.
|
it sometimes takes multiple tries for the app to refresh to the latest version.
|
||||||
You can see the version information at the bottom of this page; the best
|
You can see the version information at the bottom of this page; the best
|
||||||
way to determine the latest version is to open TimeSafari.app in an incognito/private
|
way to determine the latest version is to open this page in an incognito/private
|
||||||
browser window and look at the version there.
|
browser window and look at the version there.
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
@@ -467,6 +468,9 @@
|
|||||||
</ul>
|
</ul>
|
||||||
Then reload Time Safari.
|
Then reload Time Safari.
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
Restart your device.
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p>
|
<p>
|
||||||
If you still have problems, you can clear the cache (see "erase my data" above)
|
If you still have problems, you can clear the cache (see "erase my data" above)
|
||||||
@@ -504,12 +508,16 @@
|
|||||||
</p>
|
</p>
|
||||||
<ul class="list-disc list-outside ml-4">
|
<ul class="list-disc list-outside ml-4">
|
||||||
<li>
|
<li>
|
||||||
If sending images, a server stores them. They can be removed by editing each claim
|
If using notifications, a server stores push token data. That can be revoked at any time
|
||||||
and deleting the image.
|
by disabling notifications on the Profile <font-awesome icon="circle-user" class="fa-fw" /> page.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
If sending images, a server stores them, too. They can be removed by editing the claim
|
||||||
|
and deleting them.
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
If sending other partner system data (eg. to Trustroots) a public key and message
|
If sending other partner system data (eg. to Trustroots) a public key and message
|
||||||
data are stored on a server. Those can be removed via direct personal request (via contact below).
|
data are stored on a server. Those can be removed via direct personal request.
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
For all other claim data,
|
For all other claim data,
|
||||||
@@ -584,16 +592,15 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
import { Router } from "vue-router";
|
import { Router } from "vue-router";
|
||||||
import { copyToClipboard } from "../services/ClipboardService";
|
import { useClipboard } from "@vueuse/core";
|
||||||
// Capacitor import removed - using QRNavigationService instead
|
// Capacitor import removed - using QRNavigationService instead
|
||||||
|
|
||||||
import * as Package from "../../package.json";
|
import * as Package from "../../package.json";
|
||||||
import QuickNav from "../components/QuickNav.vue";
|
import QuickNav from "../components/QuickNav.vue";
|
||||||
import { APP_SERVER, NotificationIface } from "../constants/app";
|
import { APP_SERVER } from "../constants/app";
|
||||||
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
||||||
import { QRNavigationService } from "@/services/QRNavigationService";
|
import { QRNavigationService } from "@/services/QRNavigationService";
|
||||||
import { UNNAMED_ENTITY_NAME } from "@/constants/entities";
|
import { UNNAMED_ENTITY_NAME } from "@/constants/entities";
|
||||||
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HelpView.vue - Comprehensive Help System Component
|
* HelpView.vue - Comprehensive Help System Component
|
||||||
@@ -627,10 +634,8 @@ import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
|
|||||||
})
|
})
|
||||||
export default class HelpView extends Vue {
|
export default class HelpView extends Vue {
|
||||||
$router!: Router;
|
$router!: Router;
|
||||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
|
||||||
|
|
||||||
package = Package;
|
package = Package;
|
||||||
notify!: ReturnType<typeof createNotifyHelpers>;
|
|
||||||
commitHash = import.meta.env.VITE_GIT_HASH;
|
commitHash = import.meta.env.VITE_GIT_HASH;
|
||||||
showAlpha = false;
|
showAlpha = false;
|
||||||
showBasics = false;
|
showBasics = false;
|
||||||
@@ -643,13 +648,6 @@ export default class HelpView extends Vue {
|
|||||||
APP_SERVER = APP_SERVER;
|
APP_SERVER = APP_SERVER;
|
||||||
// Capacitor reference removed - using QRNavigationService instead
|
// Capacitor reference removed - using QRNavigationService instead
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize notification helpers
|
|
||||||
*/
|
|
||||||
created() {
|
|
||||||
this.notify = createNotifyHelpers(this.$notify);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the unnamed entity name constant
|
* Get the unnamed entity name constant
|
||||||
*/
|
*/
|
||||||
@@ -670,15 +668,11 @@ export default class HelpView extends Vue {
|
|||||||
* @param {string} text - The text to copy to clipboard
|
* @param {string} text - The text to copy to clipboard
|
||||||
* @param {Function} fn - Callback function to execute before and after copying
|
* @param {Function} fn - Callback function to execute before and after copying
|
||||||
*/
|
*/
|
||||||
async doCopyTwoSecRedo(text: string, fn: () => void): Promise<void> {
|
doCopyTwoSecRedo(text: string, fn: () => void): void {
|
||||||
fn();
|
fn();
|
||||||
try {
|
useClipboard()
|
||||||
await copyToClipboard(text);
|
.copy(text)
|
||||||
setTimeout(fn, 2000);
|
.then(() => setTimeout(fn, 2000));
|
||||||
} catch (error) {
|
|
||||||
this.$logAndConsole(`Error copying to clipboard: ${error}`, true);
|
|
||||||
this.notify.error("Failed to copy to clipboard.", TIMEOUTS.SHORT);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -170,10 +170,10 @@ Raymer * @version 1.0.0 */
|
|||||||
class="border-t p-2 border-slate-300"
|
class="border-t p-2 border-slate-300"
|
||||||
@click="goToActivityToUserPage()"
|
@click="goToActivityToUserPage()"
|
||||||
>
|
>
|
||||||
<div class="flex justify-center">
|
<div class="flex justify-center gap-2">
|
||||||
<div
|
<div
|
||||||
v-if="numNewOffersToUser"
|
v-if="numNewOffersToUser"
|
||||||
class="bg-gradient-to-b from-green-400 to-green-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] m-1 px-4 py-4 rounded-md text-white"
|
class="bg-gradient-to-b from-green-400 to-green-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] flex-1 px-4 py-4 rounded-md text-white cursor-pointer"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="block text-center text-6xl"
|
class="block text-center text-6xl"
|
||||||
@@ -187,7 +187,7 @@ Raymer * @version 1.0.0 */
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="numNewOffersToUserProjects"
|
v-if="numNewOffersToUserProjects"
|
||||||
class="bg-gradient-to-b from-green-400 to-green-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] m-1 px-4 py-4 rounded-md text-white"
|
class="bg-gradient-to-b from-green-400 to-green-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] flex-1 px-4 py-4 rounded-md text-white cursor-pointer"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="block text-center text-6xl"
|
class="block text-center text-6xl"
|
||||||
@@ -201,6 +201,22 @@ Raymer * @version 1.0.0 */
|
|||||||
projects
|
projects
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="numNewStarredProjectChanges"
|
||||||
|
class="bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] flex-1 px-4 py-4 rounded-md text-white cursor-pointer"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="block text-center text-6xl"
|
||||||
|
data-testId="newStarredProjectChangesActivityNumber"
|
||||||
|
>
|
||||||
|
{{ numNewStarredProjectChanges
|
||||||
|
}}{{ newStarredProjectChangesHitLimit ? "+" : "" }}
|
||||||
|
</span>
|
||||||
|
<p class="text-center">
|
||||||
|
favorite project{{ numNewStarredProjectChanges === 1 ? "" : "s" }}
|
||||||
|
with changes
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-end mt-2">
|
<div class="flex justify-end mt-2">
|
||||||
<button class="text-blue-500">View All New Activity For You</button>
|
<button class="text-blue-500">View All New Activity For You</button>
|
||||||
@@ -268,6 +284,7 @@ import {
|
|||||||
getHeaders,
|
getHeaders,
|
||||||
getNewOffersToUser,
|
getNewOffersToUser,
|
||||||
getNewOffersToUserProjects,
|
getNewOffersToUserProjects,
|
||||||
|
getStarredProjectsWithChanges,
|
||||||
getPlanFromCache,
|
getPlanFromCache,
|
||||||
} from "../libs/endorserServer";
|
} from "../libs/endorserServer";
|
||||||
import {
|
import {
|
||||||
@@ -283,6 +300,7 @@ import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
|
|||||||
import { NOTIFY_CONTACT_LOADING_ISSUE } from "@/constants/notifications";
|
import { NOTIFY_CONTACT_LOADING_ISSUE } from "@/constants/notifications";
|
||||||
import * as Package from "../../package.json";
|
import * as Package from "../../package.json";
|
||||||
import { UNNAMED_ENTITY_NAME } from "@/constants/entities";
|
import { UNNAMED_ENTITY_NAME } from "@/constants/entities";
|
||||||
|
import * as databaseUtil from "../db/databaseUtil";
|
||||||
|
|
||||||
// consolidate this with GiveActionClaim in src/interfaces/claims.ts
|
// consolidate this with GiveActionClaim in src/interfaces/claims.ts
|
||||||
interface Claim {
|
interface Claim {
|
||||||
@@ -395,10 +413,14 @@ export default class HomeView extends Vue {
|
|||||||
isRegistered = false;
|
isRegistered = false;
|
||||||
lastAckedOfferToUserJwtId?: string; // the last JWT ID for offer-to-user that they've acknowledged seeing
|
lastAckedOfferToUserJwtId?: string; // the last JWT ID for offer-to-user that they've acknowledged seeing
|
||||||
lastAckedOfferToUserProjectsJwtId?: string; // the last JWT ID for offers-to-user's-projects that they've acknowledged seeing
|
lastAckedOfferToUserProjectsJwtId?: string; // the last JWT ID for offers-to-user's-projects that they've acknowledged seeing
|
||||||
|
lastAckedStarredPlanChangesJwtId?: string; // the last JWT ID for starred project changes that they've acknowledged seeing
|
||||||
newOffersToUserHitLimit: boolean = false;
|
newOffersToUserHitLimit: boolean = false;
|
||||||
newOffersToUserProjectsHitLimit: boolean = false;
|
newOffersToUserProjectsHitLimit: boolean = false;
|
||||||
|
newStarredProjectChangesHitLimit: boolean = false;
|
||||||
numNewOffersToUser: number = 0; // number of new offers-to-user
|
numNewOffersToUser: number = 0; // number of new offers-to-user
|
||||||
numNewOffersToUserProjects: number = 0; // number of new offers-to-user's-projects
|
numNewOffersToUserProjects: number = 0; // number of new offers-to-user's-projects
|
||||||
|
numNewStarredProjectChanges: number = 0; // number of new starred project changes
|
||||||
|
starredPlanHandleIds: Array<string> = []; // list of starred project IDs
|
||||||
searchBoxes: Array<{
|
searchBoxes: Array<{
|
||||||
name: string;
|
name: string;
|
||||||
bbox: BoundingBox;
|
bbox: BoundingBox;
|
||||||
@@ -438,6 +460,7 @@ export default class HomeView extends Vue {
|
|||||||
// Registration check already handled in initializeIdentity()
|
// Registration check already handled in initializeIdentity()
|
||||||
await this.loadFeedData();
|
await this.loadFeedData();
|
||||||
await this.loadNewOffers();
|
await this.loadNewOffers();
|
||||||
|
await this.loadNewStarredProjectChanges();
|
||||||
await this.checkOnboarding();
|
await this.checkOnboarding();
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
this.handleError(err);
|
this.handleError(err);
|
||||||
@@ -542,8 +565,14 @@ export default class HomeView extends Vue {
|
|||||||
this.lastAckedOfferToUserJwtId = settings.lastAckedOfferToUserJwtId;
|
this.lastAckedOfferToUserJwtId = settings.lastAckedOfferToUserJwtId;
|
||||||
this.lastAckedOfferToUserProjectsJwtId =
|
this.lastAckedOfferToUserProjectsJwtId =
|
||||||
settings.lastAckedOfferToUserProjectsJwtId;
|
settings.lastAckedOfferToUserProjectsJwtId;
|
||||||
|
this.lastAckedStarredPlanChangesJwtId =
|
||||||
|
settings.lastAckedStarredPlanChangesJwtId;
|
||||||
this.searchBoxes = settings.searchBoxes || [];
|
this.searchBoxes = settings.searchBoxes || [];
|
||||||
this.showShortcutBvc = !!settings.showShortcutBvc;
|
this.showShortcutBvc = !!settings.showShortcutBvc;
|
||||||
|
this.starredPlanHandleIds = databaseUtil.parseJsonField(
|
||||||
|
settings.starredPlanHandleIds,
|
||||||
|
[],
|
||||||
|
);
|
||||||
this.isAnyFeedFilterOn = checkIsAnyFeedFilterOn(settings);
|
this.isAnyFeedFilterOn = checkIsAnyFeedFilterOn(settings);
|
||||||
|
|
||||||
// Check onboarding status
|
// Check onboarding status
|
||||||
@@ -675,6 +704,43 @@ export default class HomeView extends Vue {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads new changes for starred projects
|
||||||
|
* Updates:
|
||||||
|
* - Number of new starred project changes
|
||||||
|
* - Rate limit status for starred project changes
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
* Called by mounted() and initializeIdentity()
|
||||||
|
* @requires Active DID
|
||||||
|
*/
|
||||||
|
private async loadNewStarredProjectChanges() {
|
||||||
|
if (this.activeDid && this.starredPlanHandleIds.length > 0) {
|
||||||
|
try {
|
||||||
|
const starredProjectChanges = await getStarredProjectsWithChanges(
|
||||||
|
this.axios,
|
||||||
|
this.apiServer,
|
||||||
|
this.activeDid,
|
||||||
|
this.starredPlanHandleIds,
|
||||||
|
this.lastAckedStarredPlanChangesJwtId,
|
||||||
|
);
|
||||||
|
this.numNewStarredProjectChanges = starredProjectChanges.data.length;
|
||||||
|
this.newStarredProjectChangesHitLimit = starredProjectChanges.hitLimit;
|
||||||
|
} catch (error) {
|
||||||
|
// Don't show errors for starred project changes as it's a secondary feature
|
||||||
|
logger.warn(
|
||||||
|
"[HomeView] Failed to load starred project changes:",
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
this.numNewStarredProjectChanges = 0;
|
||||||
|
this.newStarredProjectChangesHitLimit = false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.numNewStarredProjectChanges = 0;
|
||||||
|
this.newStarredProjectChangesHitLimit = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if user needs onboarding using ultra-concise mixin utilities
|
* Checks if user needs onboarding using ultra-concise mixin utilities
|
||||||
* Opens onboarding dialog if not completed
|
* Opens onboarding dialog if not completed
|
||||||
|
|||||||
@@ -88,15 +88,9 @@ import { Router } from "vue-router";
|
|||||||
|
|
||||||
import { AppString, NotificationIface } from "../constants/app";
|
import { AppString, NotificationIface } from "../constants/app";
|
||||||
import { DEFAULT_ROOT_DERIVATION_PATH } from "../libs/crypto";
|
import { DEFAULT_ROOT_DERIVATION_PATH } from "../libs/crypto";
|
||||||
import {
|
import { retrieveAccountCount, importFromMnemonic } from "../libs/util";
|
||||||
retrieveAccountCount,
|
|
||||||
importFromMnemonic,
|
|
||||||
checkForDuplicateAccount,
|
|
||||||
DUPLICATE_ACCOUNT_ERROR,
|
|
||||||
} from "../libs/util";
|
|
||||||
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
||||||
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
|
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
|
||||||
import { NOTIFY_DUPLICATE_ACCOUNT_IMPORT } from "@/constants/notifications";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Import Account View Component
|
* Import Account View Component
|
||||||
@@ -204,19 +198,6 @@ export default class ImportAccountView extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Check for duplicate account before importing
|
|
||||||
const isDuplicate = await checkForDuplicateAccount(
|
|
||||||
this.mnemonic,
|
|
||||||
this.derivationPath,
|
|
||||||
);
|
|
||||||
if (isDuplicate) {
|
|
||||||
this.notify.warning(
|
|
||||||
NOTIFY_DUPLICATE_ACCOUNT_IMPORT.message,
|
|
||||||
TIMEOUTS.LONG,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await importFromMnemonic(
|
await importFromMnemonic(
|
||||||
this.mnemonic,
|
this.mnemonic,
|
||||||
this.derivationPath,
|
this.derivationPath,
|
||||||
@@ -242,20 +223,9 @@ export default class ImportAccountView extends Vue {
|
|||||||
this.$router.push({ name: "account" });
|
this.$router.push({ name: "account" });
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
this.$logError("Import failed: " + error);
|
this.$logError("Import failed: " + error);
|
||||||
|
|
||||||
// Check if this is a duplicate account error from saveNewIdentity
|
|
||||||
const errorMessage =
|
|
||||||
error instanceof Error ? error.message : String(error);
|
|
||||||
if (errorMessage.includes(DUPLICATE_ACCOUNT_ERROR)) {
|
|
||||||
this.notify.warning(
|
|
||||||
NOTIFY_DUPLICATE_ACCOUNT_IMPORT.message,
|
|
||||||
TIMEOUTS.LONG,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.notify.error(
|
this.notify.error(
|
||||||
errorMessage || "Failed to import account.",
|
(error instanceof Error ? error.message : String(error)) ||
|
||||||
|
"Failed to import account.",
|
||||||
TIMEOUTS.LONG,
|
TIMEOUTS.LONG,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,7 +83,6 @@ import {
|
|||||||
retrieveAllAccountsMetadata,
|
retrieveAllAccountsMetadata,
|
||||||
retrieveFullyDecryptedAccount,
|
retrieveFullyDecryptedAccount,
|
||||||
saveNewIdentity,
|
saveNewIdentity,
|
||||||
checkForDuplicateAccount,
|
|
||||||
} from "../libs/util";
|
} from "../libs/util";
|
||||||
import { logger } from "../utils/logger";
|
import { logger } from "../utils/logger";
|
||||||
import { Account, AccountEncrypted } from "../db/tables/accounts";
|
import { Account, AccountEncrypted } from "../db/tables/accounts";
|
||||||
@@ -172,16 +171,6 @@ export default class ImportAccountView extends Vue {
|
|||||||
const newId = newIdentifier(address, publicHex, privateHex, newDerivPath);
|
const newId = newIdentifier(address, publicHex, privateHex, newDerivPath);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Check for duplicate account before creating
|
|
||||||
const isDuplicate = await checkForDuplicateAccount(newId.did);
|
|
||||||
if (isDuplicate) {
|
|
||||||
this.notify.warning(
|
|
||||||
"This derived account already exists. Please try a different derivation path.",
|
|
||||||
TIMEOUTS.LONG,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await saveNewIdentity(newId, mne, newDerivPath);
|
await saveNewIdentity(newId, mne, newDerivPath);
|
||||||
|
|
||||||
// record that as the active DID
|
// record that as the active DID
|
||||||
|
|||||||
@@ -128,7 +128,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
import { copyToClipboard } from "../services/ClipboardService";
|
import { useClipboard } from "@vueuse/core";
|
||||||
import { Router } from "vue-router";
|
import { Router } from "vue-router";
|
||||||
|
|
||||||
import ContactNameDialog from "../components/ContactNameDialog.vue";
|
import ContactNameDialog from "../components/ContactNameDialog.vue";
|
||||||
@@ -333,27 +333,17 @@ export default class InviteOneView extends Vue {
|
|||||||
return `${APP_SERVER}/deep-link/invite-one-accept/${jwt}`;
|
return `${APP_SERVER}/deep-link/invite-one-accept/${jwt}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async copyInviteAndNotify(inviteId: string, jwt: string) {
|
copyInviteAndNotify(inviteId: string, jwt: string) {
|
||||||
try {
|
useClipboard().copy(this.inviteLink(jwt));
|
||||||
await copyToClipboard(this.inviteLink(jwt));
|
this.notify.success(createInviteLinkCopyMessage(inviteId), TIMEOUTS.LONG);
|
||||||
this.notify.success(createInviteLinkCopyMessage(inviteId), TIMEOUTS.LONG);
|
|
||||||
} catch (error) {
|
|
||||||
this.$logAndConsole(`Error copying invite link: ${error}`, true);
|
|
||||||
this.notify.error("Failed to copy invite link.");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async showInvite(inviteId: string, redeemed: boolean, expired: boolean) {
|
showInvite(inviteId: string, redeemed: boolean, expired: boolean) {
|
||||||
try {
|
useClipboard().copy(inviteId);
|
||||||
await copyToClipboard(inviteId);
|
this.notify.success(
|
||||||
this.notify.success(
|
createInviteIdCopyMessage(inviteId, redeemed, expired),
|
||||||
createInviteIdCopyMessage(inviteId, redeemed, expired),
|
TIMEOUTS.LONG,
|
||||||
TIMEOUTS.LONG,
|
);
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
this.$logAndConsole(`Error copying invite ID: ${error}`, true);
|
|
||||||
this.notify.error("Failed to copy invite ID.");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
v-if="newOffersToUser.length > 0"
|
v-if="newOffersToUser.length > 0"
|
||||||
:icon="showOffersDetails ? 'chevron-down' : 'chevron-right'"
|
:icon="showOffersDetails ? 'chevron-down' : 'chevron-right'"
|
||||||
class="cursor-pointer ml-4 mr-4 text-lg"
|
class="cursor-pointer ml-4 mr-4 text-lg"
|
||||||
@click="expandOffersToUserAndMarkRead()"
|
@click.prevent="expandOffersToUserAndMarkRead()"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<router-link to="/recent-offers-to-user" class="text-blue-500">
|
<router-link to="/recent-offers-to-user" class="text-blue-500">
|
||||||
@@ -48,7 +48,7 @@
|
|||||||
didInfo(offer.offeredByDid, activeDid, allMyDids, allContacts)
|
didInfo(offer.offeredByDid, activeDid, allMyDids, allContacts)
|
||||||
}}</span>
|
}}</span>
|
||||||
offered
|
offered
|
||||||
<span v-if="offer.objectDescription">{{
|
<span v-if="offer.objectDescription" class="truncate">{{
|
||||||
offer.objectDescription
|
offer.objectDescription
|
||||||
}}</span
|
}}</span
|
||||||
>{{ offer.objectDescription && offer.amount ? ", and " : "" }}
|
>{{ offer.objectDescription && offer.amount ? ", and " : "" }}
|
||||||
@@ -67,10 +67,10 @@
|
|||||||
<!-- New line that appears on hover or when the offer is clicked -->
|
<!-- New line that appears on hover or when the offer is clicked -->
|
||||||
<div
|
<div
|
||||||
class="absolute left-0 w-full text-left text-gray-500 text-sm hidden group-hover:flex cursor-pointer items-center"
|
class="absolute left-0 w-full text-left text-gray-500 text-sm hidden group-hover:flex cursor-pointer items-center"
|
||||||
@click="markOffersAsReadStartingWith(offer.jwtId)"
|
@click.prevent="markOffersAsReadStartingWith(offer.jwtId)"
|
||||||
>
|
>
|
||||||
<span class="inline-block w-8 h-px bg-gray-500 mr-2" />
|
<span class="inline-block w-8 h-px bg-gray-500 mr-2" />
|
||||||
Click to keep all above as new offers
|
Click to keep all above as unread offers
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -96,7 +96,7 @@
|
|||||||
showOffersToUserProjectsDetails ? 'chevron-down' : 'chevron-right'
|
showOffersToUserProjectsDetails ? 'chevron-down' : 'chevron-right'
|
||||||
"
|
"
|
||||||
class="cursor-pointer ml-4 mr-4 text-lg"
|
class="cursor-pointer ml-4 mr-4 text-lg"
|
||||||
@click="expandOffersToUserProjectsAndMarkRead()"
|
@click.prevent="expandOffersToUserProjectsAndMarkRead()"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<router-link to="/recent-offers-to-user-projects" class="text-blue-500">
|
<router-link to="/recent-offers-to-user-projects" class="text-blue-500">
|
||||||
@@ -115,7 +115,7 @@
|
|||||||
didInfo(offer.offeredByDid, activeDid, allMyDids, allContacts)
|
didInfo(offer.offeredByDid, activeDid, allMyDids, allContacts)
|
||||||
}}</span>
|
}}</span>
|
||||||
offered
|
offered
|
||||||
<span v-if="offer.objectDescription">{{
|
<span v-if="offer.objectDescription" class="truncate">{{
|
||||||
offer.objectDescription
|
offer.objectDescription
|
||||||
}}</span
|
}}</span
|
||||||
>{{ offer.objectDescription && offer.amount ? ", and " : "" }}
|
>{{ offer.objectDescription && offer.amount ? ", and " : "" }}
|
||||||
@@ -136,10 +136,153 @@
|
|||||||
<!-- New line that appears on hover -->
|
<!-- New line that appears on hover -->
|
||||||
<div
|
<div
|
||||||
class="absolute left-0 w-full text-left text-gray-500 text-sm hidden group-hover:flex cursor-pointer items-center"
|
class="absolute left-0 w-full text-left text-gray-500 text-sm hidden group-hover:flex cursor-pointer items-center"
|
||||||
@click="markOffersToUserProjectsAsReadStartingWith(offer.jwtId)"
|
@click.prevent="
|
||||||
|
markOffersToUserProjectsAsReadStartingWith(offer.jwtId)
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<span class="inline-block w-8 h-px bg-gray-500 mr-2" />
|
<span class="inline-block w-8 h-px bg-gray-500 mr-2" />
|
||||||
Click to keep all above as new offers
|
Click to keep all above as unread offers
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Starred Projects with Changes Section -->
|
||||||
|
<div
|
||||||
|
class="flex justify-between mt-6"
|
||||||
|
data-testId="showStarredProjectChanges"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<span class="text-lg font-medium"
|
||||||
|
>{{ newStarredProjectChanges.length
|
||||||
|
}}{{ newStarredProjectChangesHitLimit ? "+" : "" }}</span
|
||||||
|
>
|
||||||
|
<span class="text-lg font-medium ml-4"
|
||||||
|
>Favorite Project{{
|
||||||
|
newStarredProjectChanges.length === 1 ? "" : "s"
|
||||||
|
}}
|
||||||
|
With Changes</span
|
||||||
|
>
|
||||||
|
<font-awesome
|
||||||
|
v-if="newStarredProjectChanges.length > 0"
|
||||||
|
:icon="
|
||||||
|
showStarredProjectChangesDetails ? 'chevron-down' : 'chevron-right'
|
||||||
|
"
|
||||||
|
class="cursor-pointer ml-4 mr-4 text-lg"
|
||||||
|
@click.prevent="expandStarredProjectChangesAndMarkRead()"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="showStarredProjectChangesDetails" class="ml-4 mt-4">
|
||||||
|
<ul class="list-disc ml-4">
|
||||||
|
<li
|
||||||
|
v-for="projectChange in newStarredProjectChanges"
|
||||||
|
:key="projectChange.plan.handleId"
|
||||||
|
class="mt-4 relative group"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<span class="font-medium">{{
|
||||||
|
projectChange.plan.name || "Unnamed Project"
|
||||||
|
}}</span>
|
||||||
|
<span
|
||||||
|
v-if="projectChange.plan.description"
|
||||||
|
class="text-gray-600 block truncate"
|
||||||
|
>
|
||||||
|
{{ projectChange.plan.description }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<router-link
|
||||||
|
:to="{
|
||||||
|
path:
|
||||||
|
'/project/' + encodeURIComponent(projectChange.plan.handleId),
|
||||||
|
}"
|
||||||
|
class="text-blue-500 flex-shrink-0"
|
||||||
|
>
|
||||||
|
<font-awesome
|
||||||
|
icon="file-lines"
|
||||||
|
class="text-blue-500 cursor-pointer"
|
||||||
|
/>
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
<!-- Show what changed -->
|
||||||
|
<div
|
||||||
|
v-if="getPlanDifferences(projectChange.plan.handleId)"
|
||||||
|
class="text-sm mt-2"
|
||||||
|
>
|
||||||
|
<div class="font-medium mb-2">Changes</div>
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table
|
||||||
|
class="w-full text-xs border-collapse border border-gray-300 rounded-lg shadow-sm bg-white"
|
||||||
|
>
|
||||||
|
<thead>
|
||||||
|
<tr class="bg-gray-50">
|
||||||
|
<th
|
||||||
|
class="border border-gray-300 px-3 py-2 text-left font-semibold text-gray-700"
|
||||||
|
></th>
|
||||||
|
<th
|
||||||
|
class="border border-gray-300 px-3 py-2 text-left font-semibold text-gray-700"
|
||||||
|
>
|
||||||
|
Previous
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
class="border border-gray-300 px-3 py-2 text-left font-semibold text-gray-700"
|
||||||
|
>
|
||||||
|
Current
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr
|
||||||
|
v-for="(difference, field) in getPlanDifferences(
|
||||||
|
projectChange.plan.handleId,
|
||||||
|
)"
|
||||||
|
:key="field"
|
||||||
|
class="hover:bg-gray-50"
|
||||||
|
>
|
||||||
|
<td
|
||||||
|
class="border border-gray-300 px-3 py-2 font-medium text-gray-800 break-words"
|
||||||
|
>
|
||||||
|
{{ getDisplayFieldName(field) }}
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
class="border border-gray-300 px-3 py-2 text-gray-600 break-words align-top"
|
||||||
|
>
|
||||||
|
<vue-markdown
|
||||||
|
v-if="field === 'description' && difference.old"
|
||||||
|
:source="formatFieldValue(difference.old)"
|
||||||
|
class="text-sm markdown-content"
|
||||||
|
/>
|
||||||
|
<span v-else>{{ formatFieldValue(difference.old) }}</span>
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
class="border border-gray-300 px-3 py-2 text-green-700 font-medium break-words align-top"
|
||||||
|
>
|
||||||
|
<vue-markdown
|
||||||
|
v-if="field === 'description' && difference.new"
|
||||||
|
:source="formatFieldValue(difference.new)"
|
||||||
|
class="text-sm markdown-content"
|
||||||
|
/>
|
||||||
|
<span v-else>{{ formatFieldValue(difference.new) }}</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else>The changes did not affect essential project data.</div>
|
||||||
|
<!-- 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.prevent="
|
||||||
|
markStarredProjectChangesAsReadStartingWith(
|
||||||
|
projectChange.plan.jwtId!,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<span class="inline-block w-8 h-px bg-gray-500 mr-2" />
|
||||||
|
Click to keep all above as unread changes
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -149,6 +292,7 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
|
import VueMarkdown from "vue-markdown-render";
|
||||||
|
|
||||||
import GiftedDialog from "../components/GiftedDialog.vue";
|
import GiftedDialog from "../components/GiftedDialog.vue";
|
||||||
import QuickNav from "../components/QuickNav.vue";
|
import QuickNav from "../components/QuickNav.vue";
|
||||||
@@ -159,20 +303,28 @@ import { Router } from "vue-router";
|
|||||||
import {
|
import {
|
||||||
OfferSummaryRecord,
|
OfferSummaryRecord,
|
||||||
OfferToPlanSummaryRecord,
|
OfferToPlanSummaryRecord,
|
||||||
|
PlanSummaryAndPreviousClaim,
|
||||||
|
PlanSummaryRecord,
|
||||||
} from "../interfaces/records";
|
} from "../interfaces/records";
|
||||||
import {
|
import {
|
||||||
didInfo,
|
didInfo,
|
||||||
|
didInfoOrNobody,
|
||||||
displayAmount,
|
displayAmount,
|
||||||
getNewOffersToUser,
|
getNewOffersToUser,
|
||||||
getNewOffersToUserProjects,
|
getNewOffersToUserProjects,
|
||||||
|
getStarredProjectsWithChanges,
|
||||||
} from "../libs/endorserServer";
|
} from "../libs/endorserServer";
|
||||||
import { retrieveAccountDids } from "../libs/util";
|
import { retrieveAccountDids } from "../libs/util";
|
||||||
import { logger } from "../utils/logger";
|
import { logger } from "../utils/logger";
|
||||||
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
||||||
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
|
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
|
||||||
|
import * as databaseUtil from "../db/databaseUtil";
|
||||||
|
import * as R from "ramda";
|
||||||
|
import { PlanActionClaim } from "../interfaces/claims";
|
||||||
|
import { GenericCredWrapper } from "@/interfaces";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: { GiftedDialog, QuickNav, EntityIcon },
|
components: { GiftedDialog, QuickNav, EntityIcon, VueMarkdown },
|
||||||
mixins: [PlatformServiceMixin],
|
mixins: [PlatformServiceMixin],
|
||||||
})
|
})
|
||||||
export default class NewActivityView extends Vue {
|
export default class NewActivityView extends Vue {
|
||||||
@@ -186,13 +338,22 @@ export default class NewActivityView extends Vue {
|
|||||||
apiServer = "";
|
apiServer = "";
|
||||||
lastAckedOfferToUserJwtId = "";
|
lastAckedOfferToUserJwtId = "";
|
||||||
lastAckedOfferToUserProjectsJwtId = "";
|
lastAckedOfferToUserProjectsJwtId = "";
|
||||||
|
lastAckedStarredPlanChangesJwtId = "";
|
||||||
newOffersToUser: Array<OfferSummaryRecord> = [];
|
newOffersToUser: Array<OfferSummaryRecord> = [];
|
||||||
newOffersToUserHitLimit = false;
|
newOffersToUserHitLimit = false;
|
||||||
newOffersToUserProjects: Array<OfferToPlanSummaryRecord> = [];
|
newOffersToUserProjects: Array<OfferToPlanSummaryRecord> = [];
|
||||||
newOffersToUserProjectsHitLimit = false;
|
newOffersToUserProjectsHitLimit = false;
|
||||||
|
newStarredProjectChanges: Array<PlanSummaryAndPreviousClaim> = [];
|
||||||
|
newStarredProjectChangesHitLimit = false;
|
||||||
|
starredPlanHandleIds: Array<string> = [];
|
||||||
|
planDifferences: Record<
|
||||||
|
string,
|
||||||
|
Record<string, { old: unknown; new: unknown }>
|
||||||
|
> = {};
|
||||||
|
|
||||||
showOffersDetails = false;
|
showOffersDetails = false;
|
||||||
showOffersToUserProjectsDetails = false;
|
showOffersToUserProjectsDetails = false;
|
||||||
|
showStarredProjectChangesDetails = false;
|
||||||
didInfo = didInfo;
|
didInfo = didInfo;
|
||||||
displayAmount = displayAmount;
|
displayAmount = displayAmount;
|
||||||
|
|
||||||
@@ -206,6 +367,12 @@ export default class NewActivityView extends Vue {
|
|||||||
this.lastAckedOfferToUserJwtId = settings.lastAckedOfferToUserJwtId || "";
|
this.lastAckedOfferToUserJwtId = settings.lastAckedOfferToUserJwtId || "";
|
||||||
this.lastAckedOfferToUserProjectsJwtId =
|
this.lastAckedOfferToUserProjectsJwtId =
|
||||||
settings.lastAckedOfferToUserProjectsJwtId || "";
|
settings.lastAckedOfferToUserProjectsJwtId || "";
|
||||||
|
this.lastAckedStarredPlanChangesJwtId =
|
||||||
|
settings.lastAckedStarredPlanChangesJwtId || "";
|
||||||
|
this.starredPlanHandleIds = databaseUtil.parseJsonField(
|
||||||
|
settings.starredPlanHandleIds,
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
this.allContacts = await this.$getAllContacts();
|
this.allContacts = await this.$getAllContacts();
|
||||||
|
|
||||||
@@ -229,6 +396,29 @@ export default class NewActivityView extends Vue {
|
|||||||
this.newOffersToUserProjects = offersToUserProjectsData.data;
|
this.newOffersToUserProjects = offersToUserProjectsData.data;
|
||||||
this.newOffersToUserProjectsHitLimit = offersToUserProjectsData.hitLimit;
|
this.newOffersToUserProjectsHitLimit = offersToUserProjectsData.hitLimit;
|
||||||
|
|
||||||
|
// Load starred project changes if user has starred projects
|
||||||
|
if (this.starredPlanHandleIds.length > 0) {
|
||||||
|
try {
|
||||||
|
const starredProjectChangesData = await getStarredProjectsWithChanges(
|
||||||
|
this.axios,
|
||||||
|
this.apiServer,
|
||||||
|
this.activeDid,
|
||||||
|
this.starredPlanHandleIds,
|
||||||
|
this.lastAckedStarredPlanChangesJwtId,
|
||||||
|
);
|
||||||
|
this.newStarredProjectChanges = starredProjectChangesData.data;
|
||||||
|
this.newStarredProjectChangesHitLimit =
|
||||||
|
starredProjectChangesData.hitLimit;
|
||||||
|
|
||||||
|
// Analyze differences between current plans and previous claims
|
||||||
|
this.analyzePlanDifferences(this.newStarredProjectChanges);
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn("Failed to load starred project changes:", error);
|
||||||
|
this.newStarredProjectChanges = [];
|
||||||
|
this.newStarredProjectChangesHitLimit = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
logger.error("Error retrieving settings & contacts:", err);
|
logger.error("Error retrieving settings & contacts:", err);
|
||||||
@@ -242,13 +432,13 @@ export default class NewActivityView extends Vue {
|
|||||||
async expandOffersToUserAndMarkRead() {
|
async expandOffersToUserAndMarkRead() {
|
||||||
this.showOffersDetails = !this.showOffersDetails;
|
this.showOffersDetails = !this.showOffersDetails;
|
||||||
if (this.showOffersDetails) {
|
if (this.showOffersDetails) {
|
||||||
await this.$updateSettings({
|
await this.$saveUserSettings(this.activeDid, {
|
||||||
lastAckedOfferToUserJwtId: this.newOffersToUser[0].jwtId,
|
lastAckedOfferToUserJwtId: this.newOffersToUser[0].jwtId,
|
||||||
});
|
});
|
||||||
// note that we don't update this.lastAckedOfferToUserJwtId in case they
|
// note that we don't update this.lastAckedOfferToUserJwtId in case they
|
||||||
// later choose the last one to keep the offers as new
|
// later choose the last one to keep the offers as new
|
||||||
this.notify.info(
|
this.notify.info(
|
||||||
"The offers are marked as viewed. Click in the list to keep them as new.",
|
"The offers are marked read. Click in the list to keep them unread.",
|
||||||
TIMEOUTS.LONG,
|
TIMEOUTS.LONG,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -260,12 +450,12 @@ export default class NewActivityView extends Vue {
|
|||||||
);
|
);
|
||||||
if (index !== -1 && index < this.newOffersToUser.length - 1) {
|
if (index !== -1 && index < this.newOffersToUser.length - 1) {
|
||||||
// Set to the next offer's jwtId
|
// Set to the next offer's jwtId
|
||||||
await this.$updateSettings({
|
await this.$saveUserSettings(this.activeDid, {
|
||||||
lastAckedOfferToUserJwtId: this.newOffersToUser[index + 1].jwtId,
|
lastAckedOfferToUserJwtId: this.newOffersToUser[index + 1].jwtId,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// it's the last entry (or not found), so just keep it the same
|
// it's the last entry (or not found), so just keep it the same
|
||||||
await this.$updateSettings({
|
await this.$saveUserSettings(this.activeDid, {
|
||||||
lastAckedOfferToUserJwtId: this.lastAckedOfferToUserJwtId,
|
lastAckedOfferToUserJwtId: this.lastAckedOfferToUserJwtId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -279,14 +469,14 @@ export default class NewActivityView extends Vue {
|
|||||||
this.showOffersToUserProjectsDetails =
|
this.showOffersToUserProjectsDetails =
|
||||||
!this.showOffersToUserProjectsDetails;
|
!this.showOffersToUserProjectsDetails;
|
||||||
if (this.showOffersToUserProjectsDetails) {
|
if (this.showOffersToUserProjectsDetails) {
|
||||||
await this.$updateSettings({
|
await this.$saveUserSettings(this.activeDid, {
|
||||||
lastAckedOfferToUserProjectsJwtId:
|
lastAckedOfferToUserProjectsJwtId:
|
||||||
this.newOffersToUserProjects[0].jwtId,
|
this.newOffersToUserProjects[0].jwtId,
|
||||||
});
|
});
|
||||||
// note that we don't update this.lastAckedOfferToUserProjectsJwtId in case
|
// note that we don't update this.lastAckedOfferToUserProjectsJwtId in case
|
||||||
// they later choose the last one to keep the offers as new
|
// they later choose the last one to keep the offers as new
|
||||||
this.notify.info(
|
this.notify.info(
|
||||||
"The offers are now marked as viewed. Click in the list to keep them as new.",
|
"The offers are now marked read. Click in the list to keep them unread.",
|
||||||
TIMEOUTS.LONG,
|
TIMEOUTS.LONG,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -298,13 +488,13 @@ export default class NewActivityView extends Vue {
|
|||||||
);
|
);
|
||||||
if (index !== -1 && index < this.newOffersToUserProjects.length - 1) {
|
if (index !== -1 && index < this.newOffersToUserProjects.length - 1) {
|
||||||
// Set to the next offer's jwtId
|
// Set to the next offer's jwtId
|
||||||
await this.$updateSettings({
|
await this.$saveUserSettings(this.activeDid, {
|
||||||
lastAckedOfferToUserProjectsJwtId:
|
lastAckedOfferToUserProjectsJwtId:
|
||||||
this.newOffersToUserProjects[index + 1].jwtId,
|
this.newOffersToUserProjects[index + 1].jwtId,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// it's the last entry (or not found), so just keep it the same
|
// it's the last entry (or not found), so just keep it the same
|
||||||
await this.$updateSettings({
|
await this.$saveUserSettings(this.activeDid, {
|
||||||
lastAckedOfferToUserProjectsJwtId:
|
lastAckedOfferToUserProjectsJwtId:
|
||||||
this.lastAckedOfferToUserProjectsJwtId,
|
this.lastAckedOfferToUserProjectsJwtId,
|
||||||
});
|
});
|
||||||
@@ -314,5 +504,382 @@ export default class NewActivityView extends Vue {
|
|||||||
TIMEOUTS.STANDARD,
|
TIMEOUTS.STANDARD,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async expandStarredProjectChangesAndMarkRead() {
|
||||||
|
this.showStarredProjectChangesDetails =
|
||||||
|
!this.showStarredProjectChangesDetails;
|
||||||
|
if (
|
||||||
|
this.showStarredProjectChangesDetails &&
|
||||||
|
this.newStarredProjectChanges.length > 0
|
||||||
|
) {
|
||||||
|
await this.$saveUserSettings(this.activeDid, {
|
||||||
|
lastAckedStarredPlanChangesJwtId:
|
||||||
|
this.newStarredProjectChanges[0].plan.jwtId,
|
||||||
|
});
|
||||||
|
this.notify.info(
|
||||||
|
"The starred project changes are now marked read. Click in the list to keep them unread.",
|
||||||
|
TIMEOUTS.LONG,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async markStarredProjectChangesAsReadStartingWith(jwtId: string) {
|
||||||
|
const index = this.newStarredProjectChanges.findIndex(
|
||||||
|
(change) => change.plan.jwtId === jwtId,
|
||||||
|
);
|
||||||
|
if (index !== -1 && index < this.newStarredProjectChanges.length - 1) {
|
||||||
|
// Set to the next change's jwtId
|
||||||
|
await this.$saveUserSettings(this.activeDid, {
|
||||||
|
lastAckedStarredPlanChangesJwtId:
|
||||||
|
this.newStarredProjectChanges[index + 1].plan.jwtId,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// it's the last entry (or not found), so just keep it the same
|
||||||
|
await this.$saveUserSettings(this.activeDid, {
|
||||||
|
lastAckedStarredPlanChangesJwtId: this.lastAckedStarredPlanChangesJwtId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.notify.info(
|
||||||
|
"All starred project changes above that line are marked as unread.",
|
||||||
|
TIMEOUTS.STANDARD,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Analyzes differences between current plans and their previous claims
|
||||||
|
*
|
||||||
|
* Walks through a list of PlanSummaryAndPreviousClaim items and stores the
|
||||||
|
* differences between the previous claim and the current plan. This method
|
||||||
|
* extracts the claim from the wrappedClaimBefore object and compares relevant
|
||||||
|
* fields with the current plan.
|
||||||
|
*
|
||||||
|
* @param planChanges Array of PlanSummaryAndPreviousClaim objects to analyze
|
||||||
|
*/
|
||||||
|
analyzePlanDifferences(planChanges: Array<PlanSummaryAndPreviousClaim>) {
|
||||||
|
this.planDifferences = {};
|
||||||
|
|
||||||
|
for (const planChange of planChanges) {
|
||||||
|
const currentPlan: PlanSummaryRecord = planChange.plan;
|
||||||
|
const wrappedClaim: GenericCredWrapper<PlanActionClaim> =
|
||||||
|
planChange.wrappedClaimBefore;
|
||||||
|
|
||||||
|
// Extract the actual claim from the wrapped claim
|
||||||
|
let previousClaim: PlanActionClaim;
|
||||||
|
|
||||||
|
const embeddedClaim: PlanActionClaim = wrappedClaim.claim;
|
||||||
|
if (
|
||||||
|
embeddedClaim &&
|
||||||
|
typeof embeddedClaim === "object" &&
|
||||||
|
"credentialSubject" in embeddedClaim
|
||||||
|
) {
|
||||||
|
// It's a Verifiable Credential
|
||||||
|
previousClaim =
|
||||||
|
(embeddedClaim.credentialSubject as PlanActionClaim) || embeddedClaim;
|
||||||
|
} else {
|
||||||
|
// It's a direct claim
|
||||||
|
previousClaim = embeddedClaim;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!previousClaim || !currentPlan.handleId) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const differences: Record<string, { old: unknown; new: unknown }> = {};
|
||||||
|
|
||||||
|
// Compare name
|
||||||
|
const normalizedOldName = this.normalizeValueForComparison(
|
||||||
|
previousClaim.name,
|
||||||
|
);
|
||||||
|
const normalizedNewName = this.normalizeValueForComparison(
|
||||||
|
currentPlan.name,
|
||||||
|
);
|
||||||
|
if (!R.equals(normalizedOldName, normalizedNewName)) {
|
||||||
|
differences.name = {
|
||||||
|
old: previousClaim.name,
|
||||||
|
new: currentPlan.name,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare description
|
||||||
|
const normalizedOldDescription = this.normalizeValueForComparison(
|
||||||
|
previousClaim.description,
|
||||||
|
);
|
||||||
|
const normalizedNewDescription = this.normalizeValueForComparison(
|
||||||
|
currentPlan.description,
|
||||||
|
);
|
||||||
|
if (!R.equals(normalizedOldDescription, normalizedNewDescription)) {
|
||||||
|
differences.description = {
|
||||||
|
old: previousClaim.description,
|
||||||
|
new: currentPlan.description,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare location (combine latitude and longitude into one row)
|
||||||
|
const oldLat = this.normalizeValueForComparison(
|
||||||
|
previousClaim.location?.geo?.latitude,
|
||||||
|
);
|
||||||
|
const oldLon = this.normalizeValueForComparison(
|
||||||
|
previousClaim.location?.geo?.longitude,
|
||||||
|
);
|
||||||
|
const newLat = this.normalizeValueForComparison(currentPlan.locLat);
|
||||||
|
const newLon = this.normalizeValueForComparison(currentPlan.locLon);
|
||||||
|
|
||||||
|
if (!R.equals(oldLat, newLat) || !R.equals(oldLon, newLon)) {
|
||||||
|
differences.location = {
|
||||||
|
old: this.formatLocationValue(oldLat, oldLon, true),
|
||||||
|
new: this.formatLocationValue(newLat, newLon, false),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare agent (issuer)
|
||||||
|
const oldAgent = didInfoOrNobody(
|
||||||
|
previousClaim.agent?.identifier,
|
||||||
|
this.activeDid,
|
||||||
|
this.allMyDids,
|
||||||
|
this.allContacts,
|
||||||
|
);
|
||||||
|
const newAgent = didInfoOrNobody(
|
||||||
|
currentPlan.agentDid,
|
||||||
|
this.activeDid,
|
||||||
|
this.allMyDids,
|
||||||
|
this.allContacts,
|
||||||
|
);
|
||||||
|
const normalizedOldAgent = this.normalizeValueForComparison(oldAgent);
|
||||||
|
const normalizedNewAgent = this.normalizeValueForComparison(newAgent);
|
||||||
|
if (!R.equals(normalizedOldAgent, normalizedNewAgent)) {
|
||||||
|
differences.agent = {
|
||||||
|
old: oldAgent,
|
||||||
|
new: newAgent,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare start time
|
||||||
|
const oldStartTime = previousClaim.startTime;
|
||||||
|
const newStartTime = currentPlan.startTime;
|
||||||
|
const normalizedOldStartTime =
|
||||||
|
this.normalizeDateForComparison(oldStartTime);
|
||||||
|
const normalizedNewStartTime =
|
||||||
|
this.normalizeDateForComparison(newStartTime);
|
||||||
|
if (!R.equals(normalizedOldStartTime, normalizedNewStartTime)) {
|
||||||
|
differences.startTime = {
|
||||||
|
old: oldStartTime,
|
||||||
|
new: newStartTime,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare end time
|
||||||
|
const oldEndTime = previousClaim.endTime;
|
||||||
|
const newEndTime = currentPlan.endTime;
|
||||||
|
const normalizedOldEndTime = this.normalizeDateForComparison(oldEndTime);
|
||||||
|
const normalizedNewEndTime = this.normalizeDateForComparison(newEndTime);
|
||||||
|
if (!R.equals(normalizedOldEndTime, normalizedNewEndTime)) {
|
||||||
|
differences.endTime = {
|
||||||
|
old: oldEndTime,
|
||||||
|
new: newEndTime,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare image
|
||||||
|
const oldImage = previousClaim.image;
|
||||||
|
const newImage = currentPlan.image;
|
||||||
|
const normalizedOldImage = this.normalizeValueForComparison(oldImage);
|
||||||
|
const normalizedNewImage = this.normalizeValueForComparison(newImage);
|
||||||
|
if (!R.equals(normalizedOldImage, normalizedNewImage)) {
|
||||||
|
differences.image = {
|
||||||
|
old: oldImage,
|
||||||
|
new: newImage,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare url
|
||||||
|
const oldUrl = previousClaim.url;
|
||||||
|
const newUrl = currentPlan.url;
|
||||||
|
const normalizedOldUrl = this.normalizeValueForComparison(oldUrl);
|
||||||
|
const normalizedNewUrl = this.normalizeValueForComparison(newUrl);
|
||||||
|
if (!R.equals(normalizedOldUrl, normalizedNewUrl)) {
|
||||||
|
differences.url = {
|
||||||
|
old: oldUrl,
|
||||||
|
new: newUrl,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store differences if any were found
|
||||||
|
if (!R.isEmpty(differences)) {
|
||||||
|
this.planDifferences[currentPlan.handleId] = differences;
|
||||||
|
logger.debug(
|
||||||
|
"[NewActivityView] Plan differences found for",
|
||||||
|
currentPlan.handleId,
|
||||||
|
differences,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
"[NewActivityView] Analyzed",
|
||||||
|
planChanges.length,
|
||||||
|
"plan changes, found differences in",
|
||||||
|
Object.keys(this.planDifferences).length,
|
||||||
|
"plans",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalizes values for comparison - treats null, undefined, and empty string as equivalent
|
||||||
|
*
|
||||||
|
* @param value The value to normalize
|
||||||
|
* @returns The normalized value (null for null/undefined/empty, otherwise the original value)
|
||||||
|
*/
|
||||||
|
normalizeValueForComparison<T>(value: T | null | undefined): T | null {
|
||||||
|
if (value === null || value === undefined || value === "") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalizes date values for comparison by converting strings to Date objects
|
||||||
|
* Returns null for null/undefined/empty values, Date objects for valid date strings
|
||||||
|
*/
|
||||||
|
normalizeDateForComparison(value: unknown): Date | null {
|
||||||
|
if (value === null || value === undefined || value === "") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (typeof value === "string") {
|
||||||
|
const date = new Date(value);
|
||||||
|
// Check if the date is valid
|
||||||
|
return isNaN(date.getTime()) ? null : date;
|
||||||
|
}
|
||||||
|
if (value instanceof Date) {
|
||||||
|
return isNaN(value.getTime()) ? null : value;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the differences for a specific plan by handle ID
|
||||||
|
*
|
||||||
|
* @param handleId The handle ID of the plan to get differences for
|
||||||
|
* @returns The differences object or null if no differences found
|
||||||
|
*/
|
||||||
|
getPlanDifferences(
|
||||||
|
handleId: string,
|
||||||
|
): Record<string, { old: unknown; new: unknown }> | null {
|
||||||
|
return this.planDifferences[handleId] || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats a field value for display in the UI
|
||||||
|
*
|
||||||
|
* @param value The value to format
|
||||||
|
* @returns A human-readable string representation
|
||||||
|
*/
|
||||||
|
formatFieldValue(value: unknown): string {
|
||||||
|
if (value === null || value === undefined) {
|
||||||
|
return "Not set";
|
||||||
|
}
|
||||||
|
if (typeof value === "string") {
|
||||||
|
const stringValue = value || "Empty";
|
||||||
|
|
||||||
|
// Check if it's a date/time string
|
||||||
|
if (this.isDateTimeString(stringValue)) {
|
||||||
|
return this.formatDateTime(stringValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if it's a URL
|
||||||
|
if (this.isUrl(stringValue)) {
|
||||||
|
return stringValue; // Keep URLs as-is for now
|
||||||
|
}
|
||||||
|
|
||||||
|
return stringValue;
|
||||||
|
}
|
||||||
|
if (typeof value === "number") {
|
||||||
|
return value.toString();
|
||||||
|
}
|
||||||
|
if (typeof value === "boolean") {
|
||||||
|
return value ? "Yes" : "No";
|
||||||
|
}
|
||||||
|
// For complex objects, stringify
|
||||||
|
const stringified = JSON.stringify(value);
|
||||||
|
return stringified;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a string appears to be a date/time string
|
||||||
|
*/
|
||||||
|
isDateTimeString(value: string): boolean {
|
||||||
|
if (!value) return false;
|
||||||
|
// Check for ISO 8601 format or other common date formats
|
||||||
|
const dateRegex = /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2})?(\.\d{3})?Z?$/;
|
||||||
|
return dateRegex.test(value) || !isNaN(Date.parse(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a string is a URL
|
||||||
|
*/
|
||||||
|
isUrl(value: string): boolean {
|
||||||
|
if (!value) return false;
|
||||||
|
try {
|
||||||
|
new URL(value);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats a date/time string for display
|
||||||
|
*/
|
||||||
|
formatDateTime(value: string): string {
|
||||||
|
try {
|
||||||
|
const date = new Date(value);
|
||||||
|
return date.toLocaleString();
|
||||||
|
} catch {
|
||||||
|
return value; // Return original if parsing fails
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a human-readable field name for display
|
||||||
|
*
|
||||||
|
* @param fieldName The internal field name
|
||||||
|
* @returns A formatted field name for display
|
||||||
|
*/
|
||||||
|
getDisplayFieldName(fieldName: string): string {
|
||||||
|
const fieldNameMap: Record<string, string> = {
|
||||||
|
name: "Name",
|
||||||
|
description: "Description",
|
||||||
|
location: "Location",
|
||||||
|
agent: "Agent",
|
||||||
|
startTime: "Start Time",
|
||||||
|
endTime: "End Time",
|
||||||
|
image: "Image",
|
||||||
|
url: "URL",
|
||||||
|
};
|
||||||
|
return fieldNameMap[fieldName] || fieldName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats location values for display
|
||||||
|
*
|
||||||
|
* @param latitude The latitude value
|
||||||
|
* @param longitude The longitude value
|
||||||
|
* @param isOldValue Whether this is the old value (true) or new value (false)
|
||||||
|
* @returns A formatted location string
|
||||||
|
*/
|
||||||
|
formatLocationValue(
|
||||||
|
latitude: number | undefined | null,
|
||||||
|
longitude: number | undefined | null,
|
||||||
|
isOldValue: boolean = false,
|
||||||
|
): string {
|
||||||
|
if (latitude == null && longitude == null) {
|
||||||
|
return "Not set";
|
||||||
|
}
|
||||||
|
// If there's any location data, show generic labels instead of coordinates
|
||||||
|
if (isOldValue) {
|
||||||
|
return "A Location";
|
||||||
|
} else {
|
||||||
|
return "New Location";
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -110,22 +110,10 @@ export default class NewEditAccountView extends Vue {
|
|||||||
* @async
|
* @async
|
||||||
*/
|
*/
|
||||||
async onClickSaveChanges() {
|
async onClickSaveChanges() {
|
||||||
// Get the current active DID to save to user-specific settings
|
await this.$updateSettings({
|
||||||
const settings = await this.$accountSettings();
|
firstName: this.givenName,
|
||||||
const activeDid = settings.activeDid;
|
lastName: "", // deprecated, pre v 0.1.3
|
||||||
|
});
|
||||||
if (activeDid) {
|
|
||||||
// Save to user-specific settings for the current identity
|
|
||||||
await this.$saveUserSettings(activeDid, {
|
|
||||||
firstName: this.givenName,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Fallback to master settings if no active DID
|
|
||||||
await this.$saveSettings({
|
|
||||||
firstName: this.givenName,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$router.back();
|
this.$router.back();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -270,7 +270,7 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
import { copyToClipboard } from "../services/ClipboardService";
|
import { useClipboard } from "@vueuse/core";
|
||||||
import { RouteLocationNormalizedLoaded, Router } from "vue-router";
|
import { RouteLocationNormalizedLoaded, Router } from "vue-router";
|
||||||
|
|
||||||
import QuickNav from "../components/QuickNav.vue";
|
import QuickNav from "../components/QuickNav.vue";
|
||||||
@@ -676,17 +676,12 @@ export default class OnboardMeetingView extends Vue {
|
|||||||
this.notify.error(message, TIMEOUTS.LONG);
|
this.notify.error(message, TIMEOUTS.LONG);
|
||||||
}
|
}
|
||||||
|
|
||||||
async copyMembersLinkToClipboard() {
|
copyMembersLinkToClipboard() {
|
||||||
try {
|
useClipboard()
|
||||||
await copyToClipboard(this.onboardMeetingMembersLink());
|
.copy(this.onboardMeetingMembersLink())
|
||||||
this.notify.info(NOTIFY_MEETING_LINK_COPIED.message, TIMEOUTS.LONG);
|
.then(() => {
|
||||||
} catch (error) {
|
this.notify.info(NOTIFY_MEETING_LINK_COPIED.message, TIMEOUTS.LONG);
|
||||||
this.$logAndConsole(
|
});
|
||||||
`Error copying meeting link to clipboard: ${error}`,
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
this.notify.error("Failed to copy meeting link to clipboard.");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -27,10 +27,18 @@
|
|||||||
>
|
>
|
||||||
<font-awesome icon="pen" class="text-sm text-blue-500 ml-2 mb-1" />
|
<font-awesome icon="pen" class="text-sm text-blue-500 ml-2 mb-1" />
|
||||||
</button>
|
</button>
|
||||||
<button title="Copy Link to Project" @click="onCopyLinkClick()">
|
<button
|
||||||
|
:title="
|
||||||
|
isStarred
|
||||||
|
? 'Remove from starred projects'
|
||||||
|
: 'Add to starred projects'
|
||||||
|
"
|
||||||
|
@click="toggleStar()"
|
||||||
|
>
|
||||||
<font-awesome
|
<font-awesome
|
||||||
icon="link"
|
:icon="isStarred ? 'star' : ['far', 'star']"
|
||||||
class="text-sm text-slate-500 ml-2 mb-1"
|
:class="isStarred ? 'text-yellow-500' : 'text-slate-500'"
|
||||||
|
class="text-sm ml-2 mb-1"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
</h2>
|
</h2>
|
||||||
@@ -58,13 +66,13 @@
|
|||||||
icon="user"
|
icon="user"
|
||||||
class="fa-fw text-slate-400"
|
class="fa-fw text-slate-400"
|
||||||
></font-awesome>
|
></font-awesome>
|
||||||
<span class="truncate inline-block max-w-[calc(100%-2rem)]">
|
<span class="truncate max-w-[calc(100%-2rem)] ml-1">
|
||||||
{{ issuerInfoObject?.displayName }}
|
{{ issuerInfoObject?.displayName }}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span
|
<span
|
||||||
v-if="!serverUtil.isHiddenDid(issuer)"
|
v-if="!serverUtil.isHiddenDid(issuer)"
|
||||||
class="inline-flex items-center"
|
class="inline-flex items-center ml-1"
|
||||||
>
|
>
|
||||||
<router-link
|
<router-link
|
||||||
:to="{
|
:to="{
|
||||||
@@ -140,18 +148,22 @@
|
|||||||
|
|
||||||
<div class="text-sm text-slate-500">
|
<div class="text-sm text-slate-500">
|
||||||
<div v-if="!expanded">
|
<div v-if="!expanded">
|
||||||
{{ truncatedDesc }}
|
<vue-markdown
|
||||||
|
:source="truncatedDesc"
|
||||||
|
class="mb-4 markdown-content"
|
||||||
|
/>
|
||||||
<a
|
<a
|
||||||
v-if="description.length >= truncateLength"
|
v-if="description.length >= truncateLength"
|
||||||
class="uppercase text-xs font-semibold text-slate-700"
|
class="mt-4 uppercase text-xs font-semibold text-blue-700 cursor-pointer"
|
||||||
@click="expandText"
|
@click="expandText"
|
||||||
>... Read More</a
|
>... Read More</a
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
{{ description }}
|
<vue-markdown :source="description" class="mb-4 markdown-content" />
|
||||||
<a
|
<a
|
||||||
class="uppercase text-xs font-semibold text-slate-700"
|
v-if="description.length >= truncateLength"
|
||||||
|
class="mt-4 uppercase text-xs font-semibold text-blue-700 cursor-pointer"
|
||||||
@click="collapseText"
|
@click="collapseText"
|
||||||
>- Read Less</a
|
>- Read Less</a
|
||||||
>
|
>
|
||||||
@@ -592,7 +604,10 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { AxiosError } from "axios";
|
import { AxiosError } from "axios";
|
||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
|
import VueMarkdown from "vue-markdown-render";
|
||||||
import { Router } from "vue-router";
|
import { Router } from "vue-router";
|
||||||
|
import { useClipboard } from "@vueuse/core";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
GenericVerifiableCredential,
|
GenericVerifiableCredential,
|
||||||
GenericCredWrapper,
|
GenericCredWrapper,
|
||||||
@@ -603,25 +618,24 @@ import {
|
|||||||
PlanSummaryRecord,
|
PlanSummaryRecord,
|
||||||
} from "../interfaces";
|
} from "../interfaces";
|
||||||
import GiftedDialog from "../components/GiftedDialog.vue";
|
import GiftedDialog from "../components/GiftedDialog.vue";
|
||||||
|
import HiddenDidDialog from "../components/HiddenDidDialog.vue";
|
||||||
import OfferDialog from "../components/OfferDialog.vue";
|
import OfferDialog from "../components/OfferDialog.vue";
|
||||||
import TopMessage from "../components/TopMessage.vue";
|
import TopMessage from "../components/TopMessage.vue";
|
||||||
import QuickNav from "../components/QuickNav.vue";
|
import QuickNav from "../components/QuickNav.vue";
|
||||||
import EntityIcon from "../components/EntityIcon.vue";
|
import EntityIcon from "../components/EntityIcon.vue";
|
||||||
import ProjectIcon from "../components/ProjectIcon.vue";
|
import ProjectIcon from "../components/ProjectIcon.vue";
|
||||||
import { NotificationIface } from "../constants/app";
|
import { APP_SERVER, NotificationIface } from "../constants/app";
|
||||||
// Removed legacy logging import - migrated to PlatformServiceMixin
|
import { UNNAMED_PROJECT } from "../constants/entities";
|
||||||
|
import { NOTIFY_CONFIRM_CLAIM } from "../constants/notifications";
|
||||||
|
import * as databaseUtil from "../db/databaseUtil";
|
||||||
import { Contact } from "../db/tables/contacts";
|
import { Contact } from "../db/tables/contacts";
|
||||||
import * as libsUtil from "../libs/util";
|
import * as libsUtil from "../libs/util";
|
||||||
import * as serverUtil from "../libs/endorserServer";
|
import * as serverUtil from "../libs/endorserServer";
|
||||||
import { retrieveAccountDids } from "../libs/util";
|
import { retrieveAccountDids } from "../libs/util";
|
||||||
import HiddenDidDialog from "../components/HiddenDidDialog.vue";
|
import { logger } from "@/utils/logger";
|
||||||
import { logger } from "../utils/logger";
|
|
||||||
import { copyToClipboard } from "../services/ClipboardService";
|
|
||||||
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
||||||
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
|
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
|
||||||
import { NOTIFY_CONFIRM_CLAIM } from "@/constants/notifications";
|
|
||||||
import { APP_SERVER } from "@/constants/app";
|
|
||||||
import { UNNAMED_PROJECT } from "@/constants/entities";
|
|
||||||
/**
|
/**
|
||||||
* Project View Component
|
* Project View Component
|
||||||
* @author Matthew Raymer
|
* @author Matthew Raymer
|
||||||
@@ -663,6 +677,7 @@ import { UNNAMED_PROJECT } from "@/constants/entities";
|
|||||||
ProjectIcon,
|
ProjectIcon,
|
||||||
QuickNav,
|
QuickNav,
|
||||||
TopMessage,
|
TopMessage,
|
||||||
|
VueMarkdown,
|
||||||
},
|
},
|
||||||
mixins: [PlatformServiceMixin],
|
mixins: [PlatformServiceMixin],
|
||||||
})
|
})
|
||||||
@@ -718,6 +733,8 @@ export default class ProjectViewView extends Vue {
|
|||||||
givesProvidedByHitLimit = false;
|
givesProvidedByHitLimit = false;
|
||||||
givesTotalsByUnit: Array<{ unit: string; amount: number }> = [];
|
givesTotalsByUnit: Array<{ unit: string; amount: number }> = [];
|
||||||
imageUrl = "";
|
imageUrl = "";
|
||||||
|
/** Whether this project is starred by the user */
|
||||||
|
isStarred = false;
|
||||||
/** Project issuer DID */
|
/** Project issuer DID */
|
||||||
issuer = "";
|
issuer = "";
|
||||||
/** Cached issuer information */
|
/** Cached issuer information */
|
||||||
@@ -756,7 +773,7 @@ export default class ProjectViewView extends Vue {
|
|||||||
totalsExpanded = false;
|
totalsExpanded = false;
|
||||||
truncatedDesc = "";
|
truncatedDesc = "";
|
||||||
/** Truncation length */
|
/** Truncation length */
|
||||||
truncateLength = 40;
|
truncateLength = 200;
|
||||||
|
|
||||||
// Utility References
|
// Utility References
|
||||||
libsUtil = libsUtil;
|
libsUtil = libsUtil;
|
||||||
@@ -805,6 +822,12 @@ export default class ProjectViewView extends Vue {
|
|||||||
}
|
}
|
||||||
this.loadProject(this.projectId, this.activeDid);
|
this.loadProject(this.projectId, this.activeDid);
|
||||||
this.loadTotals();
|
this.loadTotals();
|
||||||
|
|
||||||
|
// Check if this project is starred when settings are loaded
|
||||||
|
if (this.projectId && settings.starredPlanHandleIds) {
|
||||||
|
const starredIds = settings.starredPlanHandleIds || [];
|
||||||
|
this.isStarred = starredIds.includes(this.projectId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -817,7 +840,7 @@ export default class ProjectViewView extends Vue {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async onCopyLinkClick() {
|
onCopyLinkClick() {
|
||||||
const shortestProjectId = this.projectId.startsWith(
|
const shortestProjectId = this.projectId.startsWith(
|
||||||
serverUtil.ENDORSER_CH_HANDLE_PREFIX,
|
serverUtil.ENDORSER_CH_HANDLE_PREFIX,
|
||||||
)
|
)
|
||||||
@@ -825,13 +848,11 @@ export default class ProjectViewView extends Vue {
|
|||||||
: this.projectId;
|
: this.projectId;
|
||||||
// Use production URL for sharing to avoid localhost issues in development
|
// Use production URL for sharing to avoid localhost issues in development
|
||||||
const deepLink = `${APP_SERVER}/deep-link/project/${shortestProjectId}`;
|
const deepLink = `${APP_SERVER}/deep-link/project/${shortestProjectId}`;
|
||||||
try {
|
useClipboard()
|
||||||
await copyToClipboard(deepLink);
|
.copy(deepLink)
|
||||||
this.notify.copied("link to this project", TIMEOUTS.SHORT);
|
.then(() => {
|
||||||
} catch (error) {
|
this.notify.copied("link to this project", TIMEOUTS.SHORT);
|
||||||
this.$logAndConsole(`Error copying project link: ${error}`, true);
|
});
|
||||||
this.notify.error("Failed to copy project link.");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Isn't there a better way to make this available to the template?
|
// Isn't there a better way to make this available to the template?
|
||||||
@@ -882,7 +903,7 @@ export default class ProjectViewView extends Vue {
|
|||||||
);
|
);
|
||||||
this.issuerVisibleToDids = resp.data.issuerVisibleToDids || [];
|
this.issuerVisibleToDids = resp.data.issuerVisibleToDids || [];
|
||||||
this.name = resp.data.claim?.name || "(no name)";
|
this.name = resp.data.claim?.name || "(no name)";
|
||||||
this.description = resp.data.claim?.description || "(no description)";
|
this.description = resp.data.claim?.description || "";
|
||||||
this.truncatedDesc = this.description.slice(0, this.truncateLength);
|
this.truncatedDesc = this.description.slice(0, this.truncateLength);
|
||||||
this.latitude = resp.data.claim?.location?.geo?.latitude || 0;
|
this.latitude = resp.data.claim?.location?.geo?.latitude || 0;
|
||||||
this.longitude = resp.data.claim?.location?.geo?.longitude || 0;
|
this.longitude = resp.data.claim?.location?.geo?.longitude || 0;
|
||||||
@@ -1472,5 +1493,72 @@ export default class ProjectViewView extends Vue {
|
|||||||
this.givesTotalsByUnit.find((total) => total.unit === "HUR")?.amount || 0
|
this.givesTotalsByUnit.find((total) => total.unit === "HUR")?.amount || 0
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle the starred status of the current project
|
||||||
|
*/
|
||||||
|
async toggleStar() {
|
||||||
|
if (!this.projectId) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!this.isStarred) {
|
||||||
|
// Add to starred projects
|
||||||
|
const settings = await databaseUtil.retrieveSettingsForActiveAccount();
|
||||||
|
const starredIds = settings.starredPlanHandleIds || [];
|
||||||
|
|
||||||
|
if (!starredIds.includes(this.projectId)) {
|
||||||
|
const newStarredIds = [...starredIds, this.projectId];
|
||||||
|
const newIdsParam = JSON.stringify(newStarredIds);
|
||||||
|
const result = await databaseUtil.updateDidSpecificSettings(
|
||||||
|
this.activeDid,
|
||||||
|
// @ts-expect-error until we use SettingsWithJsonString properly
|
||||||
|
{ starredPlanHandleIds: newIdsParam },
|
||||||
|
);
|
||||||
|
if (result) {
|
||||||
|
this.isStarred = true;
|
||||||
|
} else {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log(
|
||||||
|
"Still getting a bad result from SQL update to star a project.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!settings.lastAckedStarredPlanChangesJwtId) {
|
||||||
|
await databaseUtil.updateDidSpecificSettings(this.activeDid, {
|
||||||
|
lastAckedStarredPlanChangesJwtId: settings.lastViewedClaimId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Remove from starred projects
|
||||||
|
const settings = await databaseUtil.retrieveSettingsForActiveAccount();
|
||||||
|
const starredIds = settings.starredPlanHandleIds || [];
|
||||||
|
|
||||||
|
const updatedIds = starredIds.filter((id) => id !== this.projectId);
|
||||||
|
const newIdsParam = JSON.stringify(updatedIds);
|
||||||
|
const result = await databaseUtil.updateDidSpecificSettings(
|
||||||
|
this.activeDid,
|
||||||
|
// @ts-expect-error until we use SettingsWithJsonString properly
|
||||||
|
{ starredPlanHandleIds: newIdsParam },
|
||||||
|
);
|
||||||
|
if (result) {
|
||||||
|
this.isStarred = false;
|
||||||
|
} else {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
logger.error("Got a bad result from SQL update to unstar a project.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Error toggling star status:", error);
|
||||||
|
this.$notify(
|
||||||
|
{
|
||||||
|
group: "alert",
|
||||||
|
type: "danger",
|
||||||
|
title: "Error",
|
||||||
|
text: "Failed to update starred status. Please try again.",
|
||||||
|
},
|
||||||
|
3000,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -69,17 +69,10 @@
|
|||||||
<div v-if="claimCountWithHidden > 0" class="border-b border-slate-300 pb-2">
|
<div v-if="claimCountWithHidden > 0" class="border-b border-slate-300 pb-2">
|
||||||
<span>
|
<span>
|
||||||
{{ claimCountWithHiddenText }}
|
{{ claimCountWithHiddenText }}
|
||||||
If you don't see expected info above for someone, ask them to check that
|
so if you expected but do not see details from someone then ask them to
|
||||||
their activity is visible to you (
|
check that their activity is visible to you on their Contacts
|
||||||
<font-awesome icon="arrow-up" class="fa-fw" />
|
<font-awesome icon="users" class="text-slate-500" />
|
||||||
<font-awesome icon="eye" class="fa-fw" />
|
page.
|
||||||
) on
|
|
||||||
<a
|
|
||||||
class="text-blue-500 underline cursor-pointer"
|
|
||||||
@click="copyContactsLinkToClipboard"
|
|
||||||
>
|
|
||||||
this page </a
|
|
||||||
>.
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="claimCountByUser > 0" class="border-b border-slate-300 pb-2">
|
<div v-if="claimCountByUser > 0" class="border-b border-slate-300 pb-2">
|
||||||
@@ -127,11 +120,10 @@ import { DateTime } from "luxon";
|
|||||||
import * as R from "ramda";
|
import * as R from "ramda";
|
||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
import { Router } from "vue-router";
|
import { Router } from "vue-router";
|
||||||
import { copyToClipboard } from "../services/ClipboardService";
|
|
||||||
|
|
||||||
import QuickNav from "../components/QuickNav.vue";
|
import QuickNav from "../components/QuickNav.vue";
|
||||||
import TopMessage from "../components/TopMessage.vue";
|
import TopMessage from "../components/TopMessage.vue";
|
||||||
import { NotificationIface, APP_SERVER } from "../constants/app";
|
import { NotificationIface } from "../constants/app";
|
||||||
import { Contact } from "../db/tables/contacts";
|
import { Contact } from "../db/tables/contacts";
|
||||||
import {
|
import {
|
||||||
GenericCredWrapper,
|
GenericCredWrapper,
|
||||||
@@ -156,7 +148,6 @@ import {
|
|||||||
NOTIFY_ALL_CONFIRMATIONS_ERROR,
|
NOTIFY_ALL_CONFIRMATIONS_ERROR,
|
||||||
NOTIFY_GIVE_SEND_ERROR,
|
NOTIFY_GIVE_SEND_ERROR,
|
||||||
NOTIFY_CLAIMS_SEND_ERROR,
|
NOTIFY_CLAIMS_SEND_ERROR,
|
||||||
NOTIFY_COPIED_TO_CLIPBOARD,
|
|
||||||
createConfirmationSuccessMessage,
|
createConfirmationSuccessMessage,
|
||||||
createCombinedSuccessMessage,
|
createCombinedSuccessMessage,
|
||||||
} from "@/constants/notifications";
|
} from "@/constants/notifications";
|
||||||
@@ -204,8 +195,8 @@ export default class QuickActionBvcEndView extends Vue {
|
|||||||
get claimCountWithHiddenText() {
|
get claimCountWithHiddenText() {
|
||||||
if (this.claimCountWithHidden === 0) return "";
|
if (this.claimCountWithHidden === 0) return "";
|
||||||
return this.claimCountWithHidden === 1
|
return this.claimCountWithHidden === 1
|
||||||
? "There is 1 other claim with hidden details."
|
? "There is 1 other claim with hidden details,"
|
||||||
: `There are ${this.claimCountWithHidden} other claims with hidden details.`;
|
: `There are ${this.claimCountWithHidden} other claims with hidden details,`;
|
||||||
}
|
}
|
||||||
|
|
||||||
get claimCountByUserText() {
|
get claimCountByUserText() {
|
||||||
@@ -305,23 +296,6 @@ export default class QuickActionBvcEndView extends Vue {
|
|||||||
(this.$router as Router).push(route);
|
(this.$router as Router).push(route);
|
||||||
}
|
}
|
||||||
|
|
||||||
async copyContactsLinkToClipboard() {
|
|
||||||
const deepLinkUrl = `${APP_SERVER}/deep-link/did/${this.activeDid}`;
|
|
||||||
try {
|
|
||||||
await copyToClipboard(deepLinkUrl);
|
|
||||||
this.notify.success(
|
|
||||||
NOTIFY_COPIED_TO_CLIPBOARD.message("Your info link"),
|
|
||||||
TIMEOUTS.SHORT,
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error("Failed to copy to clipboard:", error);
|
|
||||||
this.notify.error(
|
|
||||||
"Failed to copy link to clipboard. Please try again.",
|
|
||||||
TIMEOUTS.SHORT,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async record() {
|
async record() {
|
||||||
try {
|
try {
|
||||||
if (this.claimsToConfirmSelected.length > 0) {
|
if (this.claimsToConfirmSelected.length > 0) {
|
||||||
|
|||||||
@@ -106,7 +106,7 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
import { copyToClipboard } from "../services/ClipboardService";
|
import { useClipboard } from "@vueuse/core";
|
||||||
|
|
||||||
import QuickNav from "../components/QuickNav.vue";
|
import QuickNav from "../components/QuickNav.vue";
|
||||||
import { NotificationIface } from "../constants/app";
|
import { NotificationIface } from "../constants/app";
|
||||||
@@ -231,24 +231,9 @@ export default class SeedBackupView extends Vue {
|
|||||||
/**
|
/**
|
||||||
* Reveals the seed phrase to the user
|
* Reveals the seed phrase to the user
|
||||||
* Sets showSeed to true to display the sensitive seed phrase data
|
* Sets showSeed to true to display the sensitive seed phrase data
|
||||||
* Updates the hasBackedUpSeed setting to true to track that user has backed up
|
|
||||||
*/
|
*/
|
||||||
async revealSeed(): Promise<void> {
|
revealSeed(): void {
|
||||||
this.showSeed = true;
|
this.showSeed = true;
|
||||||
|
|
||||||
// Update the account setting to track that user has backed up their seed
|
|
||||||
try {
|
|
||||||
const settings = await this.$accountSettings();
|
|
||||||
if (settings.activeDid) {
|
|
||||||
await this.$saveUserSettings(settings.activeDid, {
|
|
||||||
hasBackedUpSeed: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (err: unknown) {
|
|
||||||
logger.error("Failed to update hasBackedUpSeed setting:", err);
|
|
||||||
// Don't show error to user as this is not critical to the main functionality
|
|
||||||
// The seed phrase is still revealed, just the tracking won't work
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -279,15 +264,11 @@ export default class SeedBackupView extends Vue {
|
|||||||
* @param text - The text to copy to clipboard
|
* @param text - The text to copy to clipboard
|
||||||
* @param fn - Callback function to execute for feedback (called twice - immediately and after 2 seconds)
|
* @param fn - Callback function to execute for feedback (called twice - immediately and after 2 seconds)
|
||||||
*/
|
*/
|
||||||
async doCopyTwoSecRedo(text: string, fn: () => void) {
|
doCopyTwoSecRedo(text: string, fn: () => void) {
|
||||||
fn();
|
fn();
|
||||||
try {
|
useClipboard()
|
||||||
await copyToClipboard(text);
|
.copy(text)
|
||||||
setTimeout(fn, 2000);
|
.then(() => setTimeout(fn, 2000));
|
||||||
} catch (error) {
|
|
||||||
this.$logAndConsole(`Error copying to clipboard: ${error}`, true);
|
|
||||||
this.notify.error("Failed to copy to clipboard.");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -54,7 +54,6 @@ import { generateEndorserJwtUrlForAccount } from "../libs/endorserServer";
|
|||||||
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
||||||
import { Settings } from "@/db/tables/settings";
|
import { Settings } from "@/db/tables/settings";
|
||||||
import { Account } from "@/db/tables/accounts";
|
import { Account } from "@/db/tables/accounts";
|
||||||
import { copyToClipboard } from "../services/ClipboardService";
|
|
||||||
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
|
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
|
||||||
|
|
||||||
// Constants for magic numbers
|
// Constants for magic numbers
|
||||||
@@ -100,7 +99,7 @@ export default class ShareMyContactInfoView extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const message = await this.generateContactMessage(settings, account);
|
const message = await this.generateContactMessage(settings, account);
|
||||||
await copyToClipboard(message);
|
await this.copyToClipboard(message);
|
||||||
await this.showSuccessNotifications();
|
await this.showSuccessNotifications();
|
||||||
this.navigateToContacts();
|
this.navigateToContacts();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -141,6 +140,14 @@ export default class ShareMyContactInfoView extends Vue {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copy the contact message to clipboard
|
||||||
|
*/
|
||||||
|
private async copyToClipboard(message: string): Promise<void> {
|
||||||
|
const { copyToClipboard } = await import("../services/ClipboardService");
|
||||||
|
await copyToClipboard(message);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show success notifications after copying
|
* Show success notifications after copying
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ import { didInfo, getHeaders } from "../libs/endorserServer";
|
|||||||
import { UserProfile } from "../libs/partnerServer";
|
import { UserProfile } from "../libs/partnerServer";
|
||||||
import { retrieveAccountDids } from "../libs/util";
|
import { retrieveAccountDids } from "../libs/util";
|
||||||
import { logger } from "../utils/logger";
|
import { logger } from "../utils/logger";
|
||||||
import { copyToClipboard } from "../services/ClipboardService";
|
import { useClipboard } from "@vueuse/core";
|
||||||
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
|
||||||
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
|
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
|
||||||
import { NOTIFY_PROFILE_LOAD_ERROR } from "@/constants/notifications";
|
import { NOTIFY_PROFILE_LOAD_ERROR } from "@/constants/notifications";
|
||||||
@@ -240,16 +240,14 @@ export default class UserProfileView extends Vue {
|
|||||||
* Creates a deep link to the profile and copies it to the clipboard
|
* Creates a deep link to the profile and copies it to the clipboard
|
||||||
* Shows success notification when completed
|
* Shows success notification when completed
|
||||||
*/
|
*/
|
||||||
async onCopyLinkClick() {
|
onCopyLinkClick() {
|
||||||
// Use production URL for sharing to avoid localhost issues in development
|
// Use production URL for sharing to avoid localhost issues in development
|
||||||
const deepLink = `${APP_SERVER}/deep-link/user-profile/${this.profile?.rowId}`;
|
const deepLink = `${APP_SERVER}/deep-link/user-profile/${this.profile?.rowId}`;
|
||||||
try {
|
useClipboard()
|
||||||
await copyToClipboard(deepLink);
|
.copy(deepLink)
|
||||||
this.notify.copied("profile link", TIMEOUTS.STANDARD);
|
.then(() => {
|
||||||
} catch (error) {
|
this.notify.copied("profile link", TIMEOUTS.STANDARD);
|
||||||
this.$logAndConsole(`Error copying profile link: ${error}`, true);
|
});
|
||||||
this.notify.error("Failed to copy profile link.");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -69,7 +69,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { test, expect } from '@playwright/test';
|
import { test, expect } from '@playwright/test';
|
||||||
import { createContactName, generateNewEthrUser, importUser, importUserFromAccount } from './testUtils';
|
import { UNNAMED_ENTITY_NAME } from '../src/constants/entities';
|
||||||
|
import { deleteContact, generateAndRegisterEthrUser, importUser } from './testUtils';
|
||||||
import { NOTIFY_CONTACT_INVALID_DID } from '../src/constants/notifications';
|
import { NOTIFY_CONTACT_INVALID_DID } from '../src/constants/notifications';
|
||||||
|
|
||||||
test('Check activity feed - check that server is running', async ({ page }) => {
|
test('Check activity feed - check that server is running', async ({ page }) => {
|
||||||
@@ -184,20 +185,35 @@ test('Check invalid DID shows error and redirects', async ({ page }) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('Check User 0 can register a random person', async ({ page }) => {
|
test('Check User 0 can register a random person', async ({ page }) => {
|
||||||
const newDid = await generateNewEthrUser(page); // generate a new user
|
await importUser(page, '00');
|
||||||
|
const newDid = await generateAndRegisterEthrUser(page);
|
||||||
|
expect(newDid).toContain('did:ethr:');
|
||||||
|
|
||||||
await importUserFromAccount(page, "00"); // switch to User Zero
|
await page.goto('./');
|
||||||
|
await page.getByTestId('closeOnboardingAndFinish').click();
|
||||||
|
await page.getByRole('button', { name: 'Person' }).click();
|
||||||
|
await page.getByRole('listitem').filter({ hasText: UNNAMED_ENTITY_NAME }).locator('svg').click();
|
||||||
|
await page.getByPlaceholder('What was given').fill('Gave me access!');
|
||||||
|
await page.getByRole('button', { name: 'Sign & Send' }).click();
|
||||||
|
await expect(page.getByText('That gift was recorded.')).toBeVisible();
|
||||||
|
// now ensure that alert goes away
|
||||||
|
await page.locator('div[role="alert"] button > svg.fa-xmark').click(); // dismiss alert
|
||||||
|
await expect(page.getByText('That gift was recorded.')).toBeHidden();
|
||||||
|
|
||||||
// As User Zero, add the new user as a contact
|
// now delete the contact to test that pages still do reasonable things
|
||||||
await page.goto('./contacts');
|
await deleteContact(page, newDid);
|
||||||
const contactName = createContactName(newDid);
|
// go the activity page for this new person
|
||||||
await page.getByPlaceholder('URL or DID, Name, Public Key').fill(`${newDid}, ${contactName}`);
|
await page.goto('./did/' + encodeURIComponent(newDid));
|
||||||
await expect(page.locator('button > svg.fa-plus')).toBeVisible();
|
// maybe replace by: const popupPromise = page.waitForEvent('popup');
|
||||||
await page.locator('button > svg.fa-plus').click();
|
let error;
|
||||||
await expect(page.locator('div[role="alert"] h4:has-text("Success")')).toBeVisible(); // wait for info alert to be visible…
|
try {
|
||||||
await page.locator('div[role="alert"] button > svg.fa-xmark').click(); // …and dismiss it
|
await page.waitForSelector('div[role="alert"]', { timeout: 2000 });
|
||||||
await expect(page.locator('div[role="alert"] button > svg.fa-xmark')).toBeHidden(); // ensure alert is gone
|
error = new Error('Error alert should not show.');
|
||||||
await page.locator('div[role="alert"] button:text-is("Yes")').click(); // Register new contact
|
} catch (error) {
|
||||||
await page.locator('div[role="alert"] button:text-is("No, Not Now")').click(); // Dismiss export data prompt
|
// success
|
||||||
await expect(page.locator("li", { hasText: contactName })).toBeVisible();
|
} finally {
|
||||||
|
if (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,63 +0,0 @@
|
|||||||
import { test, expect } from '@playwright/test';
|
|
||||||
import { importUserFromAccount, getTestUserData } from './testUtils';
|
|
||||||
import { NOTIFY_DUPLICATE_ACCOUNT_IMPORT } from '../src/constants/notifications';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test duplicate account import functionality
|
|
||||||
*
|
|
||||||
* This test verifies that:
|
|
||||||
* 1. A user can successfully import an account the first time
|
|
||||||
* 2. Attempting to import the same account again shows a warning message
|
|
||||||
* 3. The duplicate import is prevented
|
|
||||||
*/
|
|
||||||
test.describe('Duplicate Account Import', () => {
|
|
||||||
test('should prevent importing the same account twice', async ({ page }) => {
|
|
||||||
const userData = getTestUserData("00");
|
|
||||||
|
|
||||||
// First import - should succeed
|
|
||||||
await page.goto("./start");
|
|
||||||
await page.getByText("You have a seed").click();
|
|
||||||
await page.getByPlaceholder("Seed Phrase").fill(userData.seedPhrase);
|
|
||||||
await page.getByRole("button", { name: "Import" }).click();
|
|
||||||
|
|
||||||
// Verify first import was successful
|
|
||||||
await expect(page.getByRole("code")).toContainText(userData.did);
|
|
||||||
|
|
||||||
// Navigate back to start page for second import attempt
|
|
||||||
await page.goto("./start");
|
|
||||||
await page.getByText("You have a seed").click();
|
|
||||||
await page.getByPlaceholder("Seed Phrase").fill(userData.seedPhrase);
|
|
||||||
await page.getByRole("button", { name: "Import" }).click();
|
|
||||||
|
|
||||||
// Verify duplicate import shows warning message
|
|
||||||
// The warning can appear either from the pre-check or from the saveNewIdentity error handling
|
|
||||||
await expect(page.getByText(NOTIFY_DUPLICATE_ACCOUNT_IMPORT.message)).toBeVisible();
|
|
||||||
|
|
||||||
// Verify we're still on the import page (not redirected to account)
|
|
||||||
await expect(page.getByPlaceholder("Seed Phrase")).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should allow importing different accounts', async ({ page }) => {
|
|
||||||
const userZeroData = getTestUserData("00");
|
|
||||||
const userOneData = getTestUserData("01");
|
|
||||||
|
|
||||||
// Import first user
|
|
||||||
await page.goto("./start");
|
|
||||||
await page.getByText("You have a seed").click();
|
|
||||||
await page.getByPlaceholder("Seed Phrase").fill(userZeroData.seedPhrase);
|
|
||||||
await page.getByRole("button", { name: "Import" }).click();
|
|
||||||
|
|
||||||
// Verify first import was successful
|
|
||||||
await expect(page.getByRole("code")).toContainText(userZeroData.did);
|
|
||||||
|
|
||||||
// Navigate back to start page for second user import
|
|
||||||
await page.goto("./start");
|
|
||||||
await page.getByText("You have a seed").click();
|
|
||||||
await page.getByPlaceholder("Seed Phrase").fill(userOneData.seedPhrase);
|
|
||||||
await page.getByRole("button", { name: "Import" }).click();
|
|
||||||
|
|
||||||
// Verify second import was successful (should not show duplicate warning)
|
|
||||||
await expect(page.getByRole("code")).toContainText(userOneData.did);
|
|
||||||
await expect(page.getByText(NOTIFY_DUPLICATE_ACCOUNT_IMPORT.message)).not.toBeVisible();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -8,7 +8,6 @@
|
|||||||
* - Custom expiration date
|
* - Custom expiration date
|
||||||
* 2. The invitation appears in the list after creation
|
* 2. The invitation appears in the list after creation
|
||||||
* 3. A new user can accept the invitation and become connected
|
* 3. A new user can accept the invitation and become connected
|
||||||
* 4. The new user can create gift records from the front page
|
|
||||||
*
|
*
|
||||||
* Test Flow:
|
* Test Flow:
|
||||||
* 1. Imports User 0 (test account)
|
* 1. Imports User 0 (test account)
|
||||||
@@ -20,8 +19,6 @@
|
|||||||
* 4. Creates a new user with Ethr DID
|
* 4. Creates a new user with Ethr DID
|
||||||
* 5. Accepts the invitation as the new user
|
* 5. Accepts the invitation as the new user
|
||||||
* 6. Verifies the connection is established
|
* 6. Verifies the connection is established
|
||||||
* 7. Tests that the new user can create gift records from the front page
|
|
||||||
* 8. Verifies the gift appears in the home view
|
|
||||||
*
|
*
|
||||||
* Related Files:
|
* Related Files:
|
||||||
* - Frontend invite handling: src/libs/endorserServer.ts
|
* - Frontend invite handling: src/libs/endorserServer.ts
|
||||||
@@ -32,7 +29,7 @@
|
|||||||
* @requires ./testUtils - For user management utilities
|
* @requires ./testUtils - For user management utilities
|
||||||
*/
|
*/
|
||||||
import { test, expect } from '@playwright/test';
|
import { test, expect } from '@playwright/test';
|
||||||
import { createGiftFromFrontPageForNewUser, deleteContact, generateNewEthrUser, generateRandomString, importUser, switchToUser } from './testUtils';
|
import { deleteContact, generateNewEthrUser, generateRandomString, importUser, switchToUser } from './testUtils';
|
||||||
|
|
||||||
test('Check User 0 can invite someone', async ({ page }) => {
|
test('Check User 0 can invite someone', async ({ page }) => {
|
||||||
await importUser(page, '00');
|
await importUser(page, '00');
|
||||||
@@ -61,7 +58,4 @@ test('Check User 0 can invite someone', async ({ page }) => {
|
|||||||
await page.locator('button:has-text("Save")').click();
|
await page.locator('button:has-text("Save")').click();
|
||||||
await expect(page.locator('button:has-text("Save")')).toBeHidden();
|
await expect(page.locator('button:has-text("Save")')).toBeHidden();
|
||||||
await expect(page.locator(`li:has-text("My pal User #0")`)).toBeVisible();
|
await expect(page.locator(`li:has-text("My pal User #0")`)).toBeVisible();
|
||||||
|
|
||||||
// Verify the new user can create a gift record from the front page
|
|
||||||
const giftTitle = await createGiftFromFrontPageForNewUser(page, `Gift from new user ${neighborNum}`);
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { expect, Page } from "@playwright/test";
|
import { expect, Page } from "@playwright/test";
|
||||||
import { UNNAMED_ENTITY_NAME } from '../src/constants/entities';
|
|
||||||
|
|
||||||
// Get test user data based on the ID.
|
// Get test user data based on the ID.
|
||||||
// '01' -> user 111
|
// '01' -> user 111
|
||||||
@@ -110,7 +109,7 @@ export async function switchToUser(page: Page, did: string): Promise<void> {
|
|||||||
await page.getByTestId("didWrapper").locator('code:has-text("did:")');
|
await page.getByTestId("didWrapper").locator('code:has-text("did:")');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createContactName(did: string): string {
|
function createContactName(did: string): string {
|
||||||
return "User " + did.slice(11, 14);
|
return "User " + did.slice(11, 14);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,10 +144,35 @@ export async function generateNewEthrUser(page: Page): Promise<string> {
|
|||||||
return newDid;
|
return newDid;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Generate a new random user and register them.
|
||||||
|
// Note that this makes 000 the active user. Use switchToUser to switch to this DID.
|
||||||
|
export async function generateAndRegisterEthrUser(page: Page): Promise<string> {
|
||||||
|
const newDid = await generateNewEthrUser(page);
|
||||||
|
|
||||||
|
await importUser(page, "000"); // switch to user 000
|
||||||
|
|
||||||
|
await page.goto("./contacts");
|
||||||
|
const contactName = createContactName(newDid);
|
||||||
|
await page
|
||||||
|
.getByPlaceholder("URL or DID, Name, Public Key")
|
||||||
|
.fill(`${newDid}, ${contactName}`);
|
||||||
|
await page.locator("button > svg.fa-plus").click();
|
||||||
|
// register them
|
||||||
|
await page.locator('div[role="alert"] button:text-is("Yes")').click();
|
||||||
|
// wait for it to disappear because the next steps may depend on alerts being gone
|
||||||
|
await expect(
|
||||||
|
page.locator('div[role="alert"] button:text-is("Yes")')
|
||||||
|
).toBeHidden();
|
||||||
|
await expect(page.locator("li", { hasText: contactName })).toBeVisible();
|
||||||
|
|
||||||
|
return newDid;
|
||||||
|
}
|
||||||
|
|
||||||
// Function to generate a random string of specified length
|
// Function to generate a random string of specified length
|
||||||
|
// Note that this only generates up to 10 characters
|
||||||
export async function generateRandomString(length: number): Promise<string> {
|
export async function generateRandomString(length: number): Promise<string> {
|
||||||
return Math.random()
|
return Math.random()
|
||||||
.toString(36)
|
.toString(36) // base 36 only generates up to 10 characters
|
||||||
.substring(2, 2 + length);
|
.substring(2, 2 + length);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,7 +181,7 @@ export async function createUniqueStringsArray(
|
|||||||
count: number
|
count: number
|
||||||
): Promise<string[]> {
|
): Promise<string[]> {
|
||||||
const stringsArray: string[] = [];
|
const stringsArray: string[] = [];
|
||||||
const stringLength = 16;
|
const stringLength = 5; // max of 10; see generateRandomString
|
||||||
|
|
||||||
for (let i = 0; i < count; i++) {
|
for (let i = 0; i < count; i++) {
|
||||||
let randomString = await generateRandomString(stringLength);
|
let randomString = await generateRandomString(stringLength);
|
||||||
@@ -216,44 +240,3 @@ export function isResourceIntensiveTest(testPath: string): boolean {
|
|||||||
testPath.includes("40-add-contact")
|
testPath.includes("40-add-contact")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a gift record from the front page
|
|
||||||
* @param page - Playwright page object
|
|
||||||
* @param giftTitle - Optional custom title, defaults to "Gift " + random string
|
|
||||||
* @param amount - Optional amount, defaults to random 1-99
|
|
||||||
* @returns Promise resolving to the created gift title
|
|
||||||
*/
|
|
||||||
export async function createGiftFromFrontPageForNewUser(
|
|
||||||
page: Page,
|
|
||||||
giftTitle?: string,
|
|
||||||
amount?: number
|
|
||||||
): Promise<void> {
|
|
||||||
// Generate random values if not provided
|
|
||||||
const randomString = Math.random().toString(36).substring(2, 6);
|
|
||||||
const finalTitle = giftTitle || `Gift ${randomString}`;
|
|
||||||
const finalAmount = amount || Math.floor(Math.random() * 99) + 1;
|
|
||||||
|
|
||||||
// Navigate to home page and close onboarding
|
|
||||||
await page.goto('./');
|
|
||||||
await page.getByTestId('closeOnboardingAndFinish').click();
|
|
||||||
|
|
||||||
// Start gift creation flow
|
|
||||||
await page.getByRole('button', { name: 'Person' }).click();
|
|
||||||
await page.getByRole('listitem').filter({ hasText: UNNAMED_ENTITY_NAME }).locator('svg').click();
|
|
||||||
|
|
||||||
// Fill gift details
|
|
||||||
await page.getByPlaceholder('What was given').fill(finalTitle);
|
|
||||||
await page.getByRole('spinbutton').fill(finalAmount.toString());
|
|
||||||
|
|
||||||
// Submit gift
|
|
||||||
await page.getByRole('button', { name: 'Sign & Send' }).click();
|
|
||||||
|
|
||||||
// Verify success
|
|
||||||
await expect(page.getByText('That gift was recorded.')).toBeVisible();
|
|
||||||
await page.locator('div[role="alert"] button > svg.fa-xmark').click(); // dismiss info alert
|
|
||||||
|
|
||||||
// Verify the gift appears in the home view
|
|
||||||
await page.goto('./');
|
|
||||||
await expect(page.locator('ul#listLatestActivity li').filter({ hasText: giftTitle })).toBeVisible();
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user