Compare commits

..

29 Commits

Author SHA1 Message Date
Jose Olarte III
c969c536bf Fix: notify getting called before it's initialized
- Initialize notify earlier inside created()
2025-08-11 17:50:25 +08:00
Matthew Raymer
de47829dc2 fix: DataExportSection error
- Fixed improper referencing for PlatformServiceMixin
- Fixed case where exported data has no contact methods

authored-by: Matthew Raymer <matthew.raymer@anomalistdesign.com>
2025-08-11 08:06:09 +00:00
Jose Olarte III
91e46f435e Merge branch 'offer-validation-logic' into build-improvement 2025-08-11 15:40:11 +08:00
Matthew Raymer
d086ab2f46 Merge branch 'master' into build-improvement 2025-08-11 06:42:22 +00:00
ff61a0bdf3 chore: Bump to v 1.0.6 build 39 2025-08-10 18:37:45 -06:00
e0b9481be5 fix: Fix error with deep links trying to parse empty query parameters. 2025-08-10 18:37:07 -06:00
a11ff04afa fix: Correct success result check for saving profile. 2025-08-08 19:26:52 -06:00
e8bf8014b4 chore: remove notifications that don't work, and adjust other commentary & docs. 2025-08-08 08:52:34 -06:00
c1713e1b0b chore: Remove duplicate logic for encoding contactMethods for DB. 2025-08-08 08:51:26 -06:00
Jose Olarte III
0277b05fef Fix: offer validation prematurely closes dialog
- Transferred form validation error handling to an earlier step
- Added validation for negative input (similar to gifting forms)
- Switched amount input to component version for consistency
2025-08-08 18:21:00 +08:00
Matthew Raymer
d5db39878c Remove debug code from ShareMyContactInfoView
- Remove debug logging and window.__SHARE_CONTACT_DEBUG__ property
- Clean up eslint-disable comments and console.log statements
- Simplify mounted() method to focus on core functionality
2025-08-08 07:17:38 +00:00
Matthew Raymer
778d00c2a4 refactor(HomeView): remove unused methods and deduplicate API server calls
- Remove unused loadSettings() method (functionality moved to initializeIdentity)
- Remove unused checkRegistrationStatus() method (functionality moved to initializeIdentity)
- Deduplicate ensureCorrectApiServer() calls (now only called once in initializeIdentity)
- Clean up import statement formatting for NOTIFY_CONTACT_LOADING_ISSUE

Reduces code complexity by eliminating 66 lines of dead code while maintaining
all existing functionality. Improves maintainability by consolidating initialization
logic into a single method.
2025-08-08 06:55:59 +00:00
Matthew Raymer
4f9fb068c8 Remove unused confirmation code from ActivityListItem and HomeView
- Remove unused handleConfirmClick() and emitConfirmClaim() methods from ActivityListItem
- Remove unused canConfirm computed property and confirmerIdList prop
- Remove unused imports: isGiveClaimType, notifyWhyCannotConfirm, containsHiddenDid
- Remove unused confirmClaim() method from HomeView
- Remove unused @confirm-claim event binding and :confirmer-id-list prop
- Remove unused imports: serverUtil, NOTIFY_CONFIRMATION_ERROR

The confirmation functionality was not being used in ActivityListItem as there was no UI to trigger it. Confirmation is handled in other components like ClaimView and ConfirmGiftView.
2025-08-08 06:48:42 +00:00
Matthew Raymer
0eb8d3d50e Migrate OnboardMeetingListView to new notify system and add comprehensive documentation
- Add missing logger import to fix 'Cannot find name logger' error
- Migrate from  to createNotifyHelpers pattern with proper NotificationIface type
- Remove unnecessary SQL query from created() method for cleaner initialization
- Add educational documentation to all methods explaining workflows, API endpoints, and user experience
- Update all notify calls to use new format (error, success, confirm methods)
- Improve code maintainability with detailed method documentation
2025-08-08 04:37:00 +00:00
Matthew Raymer
f98d6c7020 Fix notify initialization and axios access errors
- ContactQRScanShowView: Move notify initialization to created() lifecycle hook
- DIDView: Remove axios getter accessing non-existent platformService.axios
- DIDView: Enhance setVisibility() with proper server API error handling
2025-08-08 02:29:13 +00:00
bcbb80e034 bump version and add "-beta" 2025-07-25 06:04:00 -06:00
64f24dc473 bump to version 1.0.5 and build 38 2025-07-25 06:02:59 -06:00
6ddde21a86 Merge pull request 'fix problem with repeated bad stringifies of contactMethods on contact export/import' (#148) from fix-contact-import-export into master
Reviewed-on: #148
2025-07-24 21:33:47 -04:00
fd0026ac2d fix problem with repeated bad stringifies of contactMethods on contact export/import 2025-07-22 15:51:17 -06:00
3fce10ae98 bump version and add "-beta", and update commit hashes in changelog 2025-07-22 11:02:12 -06:00
002f240720 bump to version 1.0.4 and build 37 2025-07-20 20:37:26 -06:00
ffe8d90161 fix: linting 2025-07-20 19:55:37 -06:00
6d6816d1a8 Merge pull request 'Deep-link fixes' (#145) from deep-link into master
Reviewed-on: #145
2025-07-15 02:49:12 -04:00
c1477d0266 Merge branch 'master' into deep-link 2025-07-14 23:42:21 -04:00
33ce6bdb72 fix: invite-one-accept deep link would not route properly 2025-07-14 20:49:40 -06:00
dc21e8dac3 bump version number and add '-beta' 2025-07-12 22:10:53 -06:00
a9a8ba217c bump to version 1.0.3 build 36 2025-07-12 22:10:07 -06:00
b0d99e7c1e fix: quick-and-dirty fix to get the correct environment variables 2025-07-12 20:17:38 -06:00
861408c7bc Consolidate deep-link paths to be derived from the same source so they don't get out of sync any more. 2025-07-03 17:01:08 -06:00
31 changed files with 3327 additions and 1124 deletions

View File

@@ -1,34 +1,122 @@
---
alwaysApply: true
---
# Rules for peaceful co-existence with developers
# Directive: Peaceful Co-Existence with Developers
do not add or commit for the user; let him control that process
## 1) Version-Control Ownership
the content of commit messages should be from the files awaiting staging
and those which have been staged. use the differences in those files
to inform the content of the commit message
* **MUST NOT** run `git add`, `git commit`, or any write action.
* **MUST** leave staging/committing to the developer.
always preview changes and commit message to use and allow me to copy and paste
✅ Preferred Commit Message Format
## 2) Source of Truth for Commit Text
Short summary in the first line (concise and high-level).
Avoid long commit bodies unless truly necessary.
* **MUST** derive messages **only** from:
✅ Valued Content in Commit Messages
* files **staged** for commit (primary), and
* files **awaiting staging** (context).
* **MUST** use the **diffs** to inform content.
* **MUST NOT** invent changes or imply work not present in diffs.
Specific fixes or features.
Symptoms or problems that were fixed.
Notes about tests passing or TS/linting errors being resolved (briefly).
## 3) Mandatory Preview Flow
❌ Avoid in Commit Messages
* **ALWAYS** present, before any real commit:
Vague terms: “improved”, “enhanced”, “better” — especially from AI.
Minor changes: small doc tweaks, one-liners, cleanup, or lint fixes.
Redundant blurbs: repeated across files or too generic.
Multiple overlapping purposes in a single commit — prefer narrow, focused commits.
Long explanations of what can be deduced from good in-line code comments.
* file list + brief per-file notes,
* a **draft commit message** (copy-paste ready),
* nothing auto-applied.
Guiding Principle
---
Let code and inline documentation speak for themselves. Use commits to highlight what isn't obvious from reading the code.
# Commit Message Format (Normative)
## A. Subject Line (required)
```
<type>(<scope>)<!>: <summary>
```
* **type** (lowercase, Conventional Commits): `feat|fix|refactor|perf|docs|test|build|chore|ci|revert`
* **scope**: optional module/package/area (e.g., `api`, `ui/login`, `db`)
* **!**: include when a breaking change is introduced
* **summary**: imperative mood, ≤ 72 chars, no trailing period
**Examples**
* `fix(api): handle null token in refresh path`
* `feat(ui/login)!: require OTP after 3 failed attempts`
## B. Body (optional, when it adds non-obvious value)
* One blank line after subject.
* Wrap at \~72 chars.
* Explain **what** and **why**, not line-by-line “how”.
* Include brief notes like tests passing or TS/lint issues resolved **only if material**.
**Body checklist**
* [ ] Problem/symptom being addressed
* [ ] High-level approach or rationale
* [ ] Risks, tradeoffs, or follow-ups (if any)
## C. Footer (optional)
* Issue refs: `Closes #123`, `Refs #456`
* Breaking change (alternative to `!`):
`BREAKING CHANGE: <impact + migration note>`
* Authors: `Co-authored-by: Name <email>`
* Security: `CVE-XXXX-YYYY: <short note>` (if applicable)
---
## Content Guidance
### Include (when relevant)
* Specific fixes/features delivered
* Symptoms/problems fixed
* Brief note that tests passed or TS/lint errors resolved
### Avoid
* Vague: *improved, enhanced, better*
* Trivialities: tiny docs, one-liners, pure lint cleanups (separate, focused commits if needed)
* Redundancy: generic blurbs repeated across files
* Multi-purpose dumps: keep commits **narrow and focused**
* Long explanations that good inline code comments already cover
**Guiding Principle:** Let code and inline docs speak. Use commits to highlight what isnt obvious.
---
# Copy-Paste Templates
## Minimal (no body)
```text
<type>(<scope>): <summary>
```
## Standard (with body & footer)
```text
<type>(<scope>)<!>: <summary>
<why-this-change?>
<what-it-does?>
<risks-or-follow-ups?>
Closes #<id>
BREAKING CHANGE: <impact + migration>
Co-authored-by: <Name> <email>
```
---
# Assistant Output Checklist (before showing the draft)
* [ ] List changed files + 12 line notes per file
* [ ] Provide **one** focused draft message (subject/body/footer)
* [ ] Subject ≤ 72 chars, imperative mood, correct `type(scope)!` syntax
* [ ] Body only if it adds non-obvious value
* [ ] No invented changes; aligns strictly with diffs
* [ ] Render as a single copy-paste block for the developer

View File

@@ -1040,7 +1040,7 @@ If you need to build manually or want to understand the individual steps:
4. Bump the version to match Android & package.json:
```
cd ios/App && xcrun agvtool new-version 35 && perl -p -i -e "s/MARKETING_VERSION = .*;/MARKETING_VERSION = 1.0.2;/g" App.xcodeproj/project.pbxproj && cd -
cd ios/App && xcrun agvtool new-version 39 && perl -p -i -e "s/MARKETING_VERSION = .*;/MARKETING_VERSION = 1.0.6;/g" App.xcodeproj/project.pbxproj && cd -
# Unfortunately this edits Info.plist directly.
#xcrun agvtool new-marketing-version 0.4.5
```
@@ -1063,11 +1063,12 @@ If you need to build manually or want to understand the individual steps:
* This will trigger a build and take time, needing user's "login" keychain password (user's login password), repeatedly.
* If it fails with `building for 'iOS', but linking in dylib (.../.pkgx/zlib.net/v1.3.0/lib/libz.1.3.dylib) built for 'macOS'` then run XCode outside that terminal (ie. not with `npx cap open ios`).
* Click Distribute -> App Store Connect
* In AppStoreConnect, add the build to the distribution: remove the current build with the "-" when you hover over it, then "Add Build" with the new build.
* In AppStoreConnect, add the build to the distribution. You may have to remove the current build with the "-" when you hover over it, then "Add Build" with the new build.
* May have to go to App Review, click Submission, then hover over the build and click "-".
* It can take 15 minutes for the build to show up in the list of builds.
* You'll probably have to "Manage" something about encryption, disallowed in France.
* Then "Save" and "Add to Review" and "Resubmit to App Review".
* Eventually it'll be "Ready for Distribution" which means
### Android Build

View File

@@ -12,9 +12,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Deep link URLs (and other prod settings)
- Error in BVC begin view
## [Unreleased]
## [1.0.6] - 2025.08.09
### Fixed
- Deep link errors where none would validate
## [1.0.5] - 2025.07.24
### Fixed
- Export & import of contacts corrupted contact methods
## [1.0.4] - 2025.07.20 - 002f2407208d56cc59c0aa7c880535ae4cbace8b
### Fixed
- Deep link for invite-one-accept
## [1.0.3] - 2025.07.12 - a9a8ba217cd6015321911e98e6843e988dc2c4ae
### Changed
- Photo is pinned to profile mode
### Fixed
- Deep link URLs (and other prod settings)
- Error in BVC begin view
## [1.0.2] - 2025.06.20 - 276e0a741bc327de3380c4e508cccb7fee58c06d

View File

@@ -137,7 +137,7 @@ See [TESTING.md](test-playwright/TESTING.md) for detailed test instructions.
Application icons are in the `assets` directory, processed by the `capacitor-assets` command.
To add a Font Awesome icon, add to main.ts and reference with `font-awesome` element and `icon` attribute with the hyphenated name.
To add a Font Awesome icon, add to fontawesome.ts and reference with `font-awesome` element and `icon` attribute with the hyphenated name.
## Other

View File

@@ -31,8 +31,8 @@ android {
applicationId "app.timesafari.app"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 35
versionName "1.0.2"
versionCode 39
versionName "1.0.6"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.

View File

@@ -403,7 +403,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 37;
CURRENT_PROJECT_VERSION = 39;
DEVELOPMENT_TEAM = GM3FS5JQPH;
ENABLE_APP_SANDBOX = NO;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
@@ -413,7 +413,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0.4;
MARKETING_VERSION = 1.0.6;
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
PRODUCT_BUNDLE_IDENTIFIER = app.timesafari;
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -430,7 +430,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 37;
CURRENT_PROJECT_VERSION = 39;
DEVELOPMENT_TEAM = GM3FS5JQPH;
ENABLE_APP_SANDBOX = NO;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
@@ -440,7 +440,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0.4;
MARKETING_VERSION = 1.0.6;
PRODUCT_BUNDLE_IDENTIFIER = app.timesafari;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";

3316
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -23,7 +23,7 @@
"auto-run:ios": "./scripts/auto-run.sh --platform=ios",
"auto-run:android": "./scripts/auto-run.sh --platform=android",
"auto-run:electron": "./scripts/auto-run.sh --platform=electron",
"build:capacitor": "VITE_GIT_HASH=$(./scripts/get-git-hash.sh) vite build --mode capacitor --config vite.config.capacitor.mts",
"build:capacitor": "VITE_GIT_HASH=`git log -1 --pretty=format:%h` vite build --mode capacitor --config vite.config.capacitor.mts",
"build:capacitor:sync": "npm run build:capacitor && npx cap sync",
"build:ios": "./scripts/build-ios.sh",
"build:ios:dev": "./scripts/build-ios.sh --dev",

View File

@@ -168,11 +168,7 @@ build_web_assets() {
local mode=$1
log_info "Building web assets for Electron (mode: $mode)"
# Get git hash using the improved function from common.sh
local git_hash=$(get_git_hash)
log_debug "Using git hash: $git_hash"
safe_execute "Building web assets" "VITE_GIT_HASH=$git_hash vite build --mode $mode --config vite.config.electron.mts"
safe_execute "Building web assets" "VITE_GIT_HASH=\$(git log -1 --pretty=format:%h) vite build --mode $mode --config vite.config.electron.mts"
}
# Sync with Capacitor

View File

@@ -203,12 +203,8 @@ execute_vite_build() {
local mode="$1"
log_info "Executing Vite build for $mode mode..."
# Get git hash using the improved function from common.sh
local git_hash=$(get_git_hash)
log_debug "Using git hash: $git_hash"
# Construct Vite build command
local vite_cmd="VITE_GIT_HASH=$git_hash npx vite build --config vite.config.web.mts"
local vite_cmd="VITE_GIT_HASH=\$(git log -1 --pretty=format:%h) npx vite build --config vite.config.web.mts"
# Add mode if not development (development is default)
if [ "$mode" != "development" ]; then
@@ -279,12 +275,8 @@ run_type_checking() {
start_dev_server() {
log_info "Starting Vite development server..."
# Get git hash using the improved function from common.sh
local git_hash=$(get_git_hash)
log_debug "Using git hash: $git_hash"
# Construct Vite dev server command
local vite_cmd="VITE_GIT_HASH=$git_hash npx vite --config vite.config.web.mts"
local vite_cmd="VITE_GIT_HASH=\$(git log -1 --pretty=format:%h) npx vite --config vite.config.web.mts"
# Add mode if specified (though development is default)
if [ "$BUILD_MODE" != "development" ]; then

View File

@@ -134,25 +134,10 @@ check_venv() {
# Function to get git hash for versioning
get_git_hash() {
# Use the dedicated git hash script for consistency
if [ -f "$(dirname "$0")/get-git-hash.sh" ]; then
"$(dirname "$0")/get-git-hash.sh"
if command -v git &> /dev/null; then
git log -1 --pretty=format:%h 2>/dev/null || echo "unknown"
else
# Fallback to direct git command if script not found
if command -v git &> /dev/null; then
# Get the current branch name
local current_branch=$(git branch --show-current 2>/dev/null || git rev-parse --abbrev-ref HEAD 2>/dev/null)
# If we're in a detached HEAD state or no branch, use HEAD
if [ -z "$current_branch" ] || [ "$current_branch" = "HEAD" ]; then
git log -1 --pretty=format:%h 2>/dev/null || echo "unknown"
else
# Use the current branch explicitly
git log -1 --pretty=format:%h "$current_branch" 2>/dev/null || echo "unknown"
fi
else
echo "unknown"
fi
echo "unknown"
fi
}

View File

@@ -1,105 +0,0 @@
#!/bin/bash
# TimeSafari Git Hash Retrieval Script
# Author: Matthew Raymer
# Description: Retrieves the current git commit hash for the active branch
#
# This script ensures that the correct git hash is retrieved regardless of
# the current branch or git state. It handles edge cases like detached HEAD
# and provides fallbacks for when git is not available.
#
# ARCHITECTURAL BENEFITS:
# - Centralized Logic: Single source of truth for git hash retrieval across all build scripts
# - Consistent Behavior: Ensures all builds use the same git hash logic and format
# - Maintainability: Changes to git hash logic only need to be made in one place
# - Robust Error Handling: Handles edge cases that could cause build failures
# - Branch-Aware: Explicitly uses current branch, preventing default branch fallback issues
#
# USAGE PATTERNS:
# # Direct usage
# ./scripts/get-git-hash.sh
#
# # In build scripts (recommended)
# VITE_GIT_HASH=$(./scripts/get-git-hash.sh) npm run build
#
# # In shell scripts
# git_hash=$(./scripts/get-git-hash.sh)
# echo "Current commit: $git_hash"
#
# # In package.json scripts
# "build:capacitor": "VITE_GIT_HASH=$(./scripts/get-git-hash.sh) vite build --mode capacitor --config vite.config.capacitor.mts"
#
# OUTPUT:
# - Git commit hash (7 characters) if available (e.g., "bf08e57c")
# - "unknown" if git is not available or no repository found
#
# EXIT CODES:
# 0 - Success (hash retrieved or "unknown" returned)
# 1 - Error (should not occur in normal operation)
#
# EDGE CASES HANDLED:
# - Detached HEAD state: Falls back to HEAD commit
# - No git repository: Returns "unknown"
# - Git not installed: Returns "unknown"
# - No commits: Returns "unknown"
# - Branch detection failure: Falls back to HEAD commit
#
# INTEGRATION POINTS:
# - scripts/common.sh: Primary usage via get_git_hash() function
# - package.json: Direct usage in build:capacitor script
# - Build scripts: Used by build-web.sh, build-electron.sh, etc.
# - Docker builds: Ensures consistent git hashes in containerized builds
#
# VALUE PROPOSITION:
# This script was created to solve git hash inconsistencies across different
# build environments and branch states. It provides a reliable, consistent
# interface for git hash retrieval that works regardless of the current
# git state or environment. This prevents issues like:
# - Builds using wrong branch's commit hash
# - Inconsistent versioning across different build types
# - Build failures due to git state issues
# - Manual git hash management in multiple scripts
#
# MAINTENANCE:
# - Update this script if git hash retrieval logic needs to change
# - All build scripts automatically benefit from improvements
# - Test with various git states (detached HEAD, different branches, etc.)
# - Ensure compatibility with CI/CD environments
set -euo pipefail
# Function to get git hash for versioning
get_git_hash() {
# Check if git is available
if ! command -v git &> /dev/null; then
echo "unknown"
return 0
fi
# Check if we're in a git repository
if ! git rev-parse --git-dir &> /dev/null; then
echo "unknown"
return 0
fi
# Get the current branch name
local current_branch
current_branch=$(git branch --show-current 2>/dev/null || git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "")
# If we're in a detached HEAD state or no branch, use HEAD
if [ -z "$current_branch" ] || [ "$current_branch" = "HEAD" ]; then
git log -1 --pretty=format:%h 2>/dev/null || echo "unknown"
else
# Use the current branch explicitly
git log -1 --pretty=format:%h "$current_branch" 2>/dev/null || echo "unknown"
fi
}
# Main execution
main() {
local git_hash
git_hash=$(get_git_hash)
echo "$git_hash"
}
# Run main function
main "$@"

View File

@@ -251,8 +251,7 @@
import { Component, Prop, Vue, Emit } from "vue-facing-decorator";
import { GiveRecordWithContactInfo } from "@/interfaces/give";
import EntityIcon from "./EntityIcon.vue";
import { isGiveClaimType, notifyWhyCannotConfirm } from "../libs/util";
import { containsHiddenDid, isHiddenDid } from "../libs/endorserServer";
import { isHiddenDid } from "../libs/endorserServer";
import ProjectIcon from "./ProjectIcon.vue";
import { createNotifyHelpers } from "@/utils/notify";
import {
@@ -272,7 +271,6 @@ export default class ActivityListItem extends Vue {
@Prop() lastViewedClaimId?: string;
@Prop() isRegistered!: boolean;
@Prop() activeDid!: string;
@Prop() confirmerIdList?: string[];
/**
* Function prop for handling image caching
@@ -331,15 +329,6 @@ export default class ActivityListItem extends Vue {
return unitCode === "HUR" ? (single ? "hour" : "hours") : unitCode;
}
get canConfirm(): boolean {
if (!this.isRegistered) return false;
if (!isGiveClaimType(this.record.fullClaim?.["@type"])) return false;
if (this.confirmerIdList?.includes(this.activeDid)) return false;
if (this.record.issuerDid === this.activeDid) return false;
if (containsHiddenDid(this.record.fullClaim)) return false;
return true;
}
// Emit methods using @Emit decorator
@Emit("viewImage")
emitViewImage(imageUrl: string) {
@@ -351,26 +340,6 @@ export default class ActivityListItem extends Vue {
return jwtId;
}
@Emit("confirmClaim")
emitConfirmClaim() {
if (!this.canConfirm) {
notifyWhyCannotConfirm(
(msg, timeout) => this.notify.info(msg.text ?? "", timeout),
this.isRegistered,
this.record.fullClaim?.["@type"],
this.record,
this.activeDid,
this.confirmerIdList,
);
return;
}
return this.record;
}
handleConfirmClick() {
this.emitConfirmClaim();
}
get friendlyDate(): string {
const date = new Date(this.record.issuedAt);
return date.toLocaleDateString(undefined, {

View File

@@ -54,7 +54,10 @@ messages * - Conditional UI based on platform capabilities * * @component *
<script lang="ts">
import { Component, Prop, Vue } from "vue-facing-decorator";
import * as R from "ramda";
import { AppString, NotificationIface } from "../constants/app";
import { Contact } from "../db/tables/contacts";
import { logger } from "../utils/logger";
import { contactsToExportJson } from "../libs/util";
@@ -179,7 +182,19 @@ export default class DataExportSection extends Vue {
const allContacts = await this.$contacts();
// Convert contacts to export format
const exportData = contactsToExportJson(allContacts);
const processedContacts: Contact[] = allContacts.map((contact) => {
// first remove the contactMethods field, mostly to cast to a clear type (that will end up with JSON objects)
const exContact: Contact = R.omit(["contactMethods"], contact);
// now add contactMethods as a true array of ContactMethod objects
exContact.contactMethods = contact.contactMethods
? (typeof contact.contactMethods === 'string' && contact.contactMethods.trim() !== ''
? JSON.parse(contact.contactMethods)
: [])
: [];
return exContact;
});
const exportData = contactsToExportJson(processedContacts);
const jsonStr = JSON.stringify(exportData, null, 2);
// Use platform service to handle export (no platform-specific logic here!)

View File

@@ -15,26 +15,25 @@ Raymer */
class="block w-full rounded border border-slate-400 mb-2 px-3 py-2"
placeholder="Description of what is offered"
/>
<div class="flex flex-row mt-2">
<span :class="unitCodeDisplayClasses" @click="changeUnitCode()">
{{ libsUtil.UNIT_SHORT[amountUnitCode] }}
</span>
<div
v-if="showDecrementButton"
:class="controlButtonClasses"
@click="decrement()"
>
<font-awesome icon="chevron-left" />
</div>
<input
v-model="amountInput"
<div class="flex mb-4">
<AmountInput
:value="parseFloat(amountInput) || 0"
:onUpdateValue="handleAmountUpdate"
data-testId="inputOfferAmount"
type="number"
:class="amountInputClasses"
/>
<div :class="incrementButtonClasses" @click="increment()">
<font-awesome icon="chevron-right" />
</div>
<select
v-model="amountUnitCode"
class="flex-1 rounded border border-slate-400 ms-2 px-3 py-2"
>
<option
v-for="(displayName, code) in unitOptions"
:key="code"
:value="code"
>
{{ displayName }}
</option>
</select>
</div>
<div class="mt-4 flex justify-center">
<span>
@@ -73,10 +72,15 @@ import {
NOTIFY_OFFER_CREATION_ERROR,
NOTIFY_OFFER_SUCCESS,
NOTIFY_OFFER_SUBMISSION_ERROR,
NOTIFY_OFFER_ERROR_NEGATIVE_AMOUNT,
} from "@/constants/notifications";
import AmountInput from "./AmountInput.vue";
@Component({
mixins: [PlatformServiceMixin],
components: {
AmountInput,
},
})
export default class OfferDialog extends Vue {
@Prop projectId?: string;
@@ -122,35 +126,10 @@ export default class OfferDialog extends Vue {
}
/**
* CSS classes for unit code selector and increment/decrement buttons
* Reduces template complexity for repeated border and styling patterns
* Computed property to get unit options for the select dropdown
*/
get controlButtonClasses(): string {
return "border border-r-0 border-slate-400 bg-slate-200 px-4 py-2";
}
/**
* CSS classes for unit code display span
* Reduces template complexity for unit code button styling
*/
get unitCodeDisplayClasses(): string {
return "rounded-l border border-r-0 border-slate-400 bg-slate-200 w-1/3 text-center text-blue-500 px-2 py-2";
}
/**
* CSS classes for amount input field
* Reduces template complexity for input styling
*/
get amountInputClasses(): string {
return "w-full border border-r-0 border-slate-400 px-2 py-2 text-center";
}
/**
* CSS classes for the right-most increment button
* Reduces template complexity for border styling
*/
get incrementButtonClasses(): string {
return "rounded-r border border-slate-400 bg-slate-200 px-4 py-2";
get unitOptions() {
return this.libsUtil.UNIT_SHORT;
}
/**
@@ -173,13 +152,7 @@ export default class OfferDialog extends Vue {
};
}
/**
* Whether the decrement button should be visible
* Encapsulates conditional logic from template
*/
get showDecrementButton(): boolean {
return this.amountInput !== "0";
}
// =================================================
// COMPONENT METHODS
@@ -226,30 +199,14 @@ export default class OfferDialog extends Vue {
this.visible = false;
}
/**
* Cycle through available unit codes
*/
changeUnitCode() {
const units = Object.keys(this.libsUtil.UNIT_SHORT);
const index = units.indexOf(this.amountUnitCode);
this.amountUnitCode = units[(index + 1) % units.length];
}
/**
* Increment the amount input
* Handle amount updates from AmountInput component
* @param value - New amount value
*/
increment() {
this.amountInput = `${(parseFloat(this.amountInput) || 0) + 1}`;
}
/**
* Decrement the amount input
*/
decrement() {
this.amountInput = `${Math.max(
0,
(parseFloat(this.amountInput) || 1) - 1,
)}`;
handleAmountUpdate(value: number) {
this.amountInput = value.toString();
}
/**
@@ -273,6 +230,28 @@ export default class OfferDialog extends Vue {
* Confirm and submit the offer
*/
async confirm() {
if (!this.activeDid) {
this.notify.error(NOTIFY_OFFER_IDENTITY_REQUIRED.message, TIMEOUTS.LONG);
return;
}
if (parseFloat(this.amountInput) < 0) {
this.notify.error(
NOTIFY_OFFER_ERROR_NEGATIVE_AMOUNT.message,
TIMEOUTS.SHORT,
);
return;
}
if (!this.description && !parseFloat(this.amountInput)) {
const message = NOTIFY_OFFER_DESCRIPTION_REQUIRED.message.replace(
"{unit}",
this.libsUtil.UNIT_LONG[this.amountUnitCode],
);
this.notify.error(message, TIMEOUTS.SHORT);
return;
}
this.close();
this.notify.toast(NOTIFY_OFFER_RECORDING.text, undefined, TIMEOUTS.BRIEF);
@@ -301,20 +280,6 @@ export default class OfferDialog extends Vue {
unitCode: string = "HUR",
expirationDateInput?: string,
) {
if (!this.activeDid) {
this.notify.error(NOTIFY_OFFER_IDENTITY_REQUIRED.message, TIMEOUTS.LONG);
return;
}
if (!description && !amount) {
const message = NOTIFY_OFFER_DESCRIPTION_REQUIRED.message.replace(
"{unit}",
this.libsUtil.UNIT_LONG[unitCode],
);
this.notify.error(message, TIMEOUTS.MODAL);
return;
}
try {
const result = await createAndSubmitOffer(
this.axios,
@@ -363,7 +328,7 @@ export default class OfferDialog extends Vue {
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
z-index: 50;
}
.dialog {

View File

@@ -33,7 +33,6 @@ export const deepLinkSchemas = {
id: z.string(),
}),
"claim-add-raw": z.object({
id: z.string(),
claim: z.string().optional(),
claimJwtId: z.string().optional(),
}),

View File

@@ -893,7 +893,7 @@ export interface DatabaseExport {
}
/**
* Converts an array of contacts to the standardized database export JSON format.
* Converts an array of contacts to the export JSON format.
* This format is used for data migration and backup purposes.
*
* @param contacts - Array of Contact objects to convert

View File

@@ -103,9 +103,10 @@ export class ProfileService {
{ headers },
);
if (response.status === 200) {
if (response.status === 201) {
return true;
} else {
logger.error("Error saving profile:", response);
throw new Error(ACCOUNT_VIEW_CONSTANTS.ERRORS.PROFILE_NOT_SAVED);
}
} catch (error) {

View File

@@ -149,6 +149,10 @@ export class DeepLinkHandler {
params[routeConfig.paramKey ?? "id"] = pathParams.join("/");
}
// logConsoleAndDb(
// `[DeepLink] Debug: Route Path: ${routePath} Path Params: ${JSON.stringify(params)} Query String: ${JSON.stringify(query)}`,
// false,
// );
return { path: routePath, params, query };
}
@@ -195,14 +199,14 @@ export class DeepLinkHandler {
// Continue with parameter validation as before...
const schema = deepLinkSchemas[path as keyof typeof deepLinkSchemas];
let validatedParams, validatedQuery;
let validatedParams;
try {
validatedParams = await schema.parseAsync(params);
validatedQuery = await schema.parseAsync(query);
} catch (error) {
// For parameter validation errors, provide specific error feedback
console.error(
logConsoleAndDb(
`[DeepLink] Invalid parameters for route name ${routeName} for path: ${path}: ${JSON.stringify(error)} ... with params: ${JSON.stringify(params)} ... and query: ${JSON.stringify(query)}`,
true,
);
await this.router.replace({
name: "deep-link-error",
@@ -223,11 +227,11 @@ export class DeepLinkHandler {
await this.router.replace({
name: routeName,
params: validatedParams,
query: validatedQuery,
});
} catch (error) {
console.error(
`[DeepLink] Error routing to route name ${routeName} for path: ${path}: ${JSON.stringify(error)} ... with validated params: ${JSON.stringify(validatedParams)} ... and validated query: ${JSON.stringify(validatedQuery)}`,
logConsoleAndDb(
`[DeepLink] Error routing to route name ${routeName} for path: ${path}: ${JSON.stringify(error)} ... with validated params: ${JSON.stringify(validatedParams)}`,
true,
);
// For parameter validation errors, provide specific error feedback
await this.router.replace({
@@ -237,7 +241,6 @@ export class DeepLinkHandler {
originalPath: path,
errorCode: "ROUTING_ERROR",
errorMessage: `Error routing to ${routeName}: ${JSON.stringify(error)}`,
...validatedQuery,
},
});
}
@@ -260,8 +263,9 @@ export class DeepLinkHandler {
await this.validateAndRoute(path, sanitizedParams, query);
} catch (error) {
const deepLinkError = error as DeepLinkError;
console.error(
logConsoleAndDb(
`[DeepLink] Error (${deepLinkError.code}): ${deepLinkError.details}`,
true,
);
throw {

View File

@@ -60,8 +60,10 @@
@share-info="onShareInfo"
/>
<!-- Notifications -->
<!-- Currently disabled because it doesn't work, even on Chrome. If restored, make sure it works or doesn't show on mobile/electron. -->
<section
v-if="isRegistered"
v-if="false"
id="sectionNotifications"
class="bg-slate-100 rounded-md overflow-hidden px-4 py-4 mt-8 mb-8"
aria-labelledby="notificationsHeading"

View File

@@ -52,7 +52,6 @@ function isApiResponse(response: unknown): response is AxiosResponse {
);
}
// TODO: Testing Required - Database Operations + Logging Migration to PlatformServiceMixin
// Priority: High | Migrated: 2025-07-06 | Author: Matthew Raymer
//
// MIGRATION DETAILS: Migrated from legacy database utilities + logging to PlatformServiceMixin

View File

@@ -724,6 +724,8 @@ export default class ClaimView extends Vue {
}
async created() {
this.notify = createNotifyHelpers(this.$notify);
const settings = await this.$accountSettings();
this.activeDid = settings.activeDid || "";
@@ -754,8 +756,6 @@ export default class ClaimView extends Vue {
this.windowDeepLink = `${APP_SERVER}/deep-link/claim/${claimId}`;
this.canShare = !!navigator.share;
this.notify = createNotifyHelpers(this.$notify);
}
// insert a space before any capital letters except the initial letter

View File

@@ -553,11 +553,6 @@ export default class ContactQRScanFull extends Vue {
return;
}
// Add new contact
// @ts-expect-error because we're just using the value to store to the DB
contact.contactMethods = JSON.stringify(
(this as any)._parseJsonField(contact.contactMethods, []),
);
await this.$insertContact(contact);
if (this.activeDid) {

View File

@@ -213,15 +213,11 @@ export default class ContactQRScanShow extends Vue {
$router!: Router;
// Notification helper system
private notify = createNotifyHelpers(this.$notify);
notify!: ReturnType<typeof createNotifyHelpers>;
activeDid = "";
apiServer = "";
// Axios instance for API calls
get axios() {
return (this as any).$platformService.axios;
}
givenName = "";
hideRegisterPromptOnNewContact = false;
isRegistered = false;
@@ -288,6 +284,8 @@ export default class ContactQRScanShow extends Vue {
}
async created() {
this.notify = createNotifyHelpers(this.$notify);
try {
const settings = await this.$accountSettings();
this.activeDid = settings.activeDid || "";

View File

@@ -273,6 +273,7 @@ import {
displayAmount,
getHeaders,
register,
setVisibilityUtil,
} from "../libs/endorserServer";
import * as libsUtil from "../libs/util";
import EntityIcon from "../components/EntityIcon.vue";
@@ -324,6 +325,7 @@ export default class DIDView extends Vue {
apiServer = "";
claims: Array<GenericCredWrapper<GenericVerifiableCredential>> = [];
contactFromDid?: Contact;
contactYaml = "";
hitEnd = false;
isLoading = false;
@@ -722,18 +724,31 @@ export default class DIDView extends Vue {
visibility: boolean,
showSuccessAlert: boolean,
) {
// Update contact visibility using mixin method
await this.$updateContact(contact.did, { seesMe: visibility });
const result = await setVisibilityUtil(
this.activeDid,
this.apiServer,
this.axios,
contact,
visibility,
);
if (showSuccessAlert) {
if (result.success) {
if (showSuccessAlert) {
const message =
(contact.name || "That user") +
" can " +
(visibility ? "" : "not ") +
"see your activity.";
this.notify.success(message, TIMEOUTS.SHORT);
}
return true;
} else {
logger.error("Got strange result from setting visibility:", result);
const message =
(contact.name || "That user") +
" can " +
(visibility ? "" : "not ") +
"see your activity.";
this.notify.success(message, TIMEOUTS.SHORT);
(result.error as string) || "Could not set visibility on the server.";
this.notify.error(message, TIMEOUTS.LONG);
return false;
}
return true;
}
/**

View File

@@ -588,10 +588,20 @@ export default class GiftedDetails extends Vue {
this.imageUrl = "";
} catch (error) {
logger.error("Error deleting image:", error);
this.notify.error(
NOTIFY_GIFTED_DETAILS_DELETE_IMAGE_ERROR.message,
TIMEOUTS.LONG,
);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if ((error as any)?.response?.status === 404) {
logger.log("Weird: the image was already deleted.", error);
localStorage.removeItem("imageUrl");
this.imageUrl = "";
// it already doesn't exist so we won't say anything to the user
} else {
this.notify.error(
NOTIFY_GIFTED_DETAILS_DELETE_IMAGE_ERROR.message,
TIMEOUTS.LONG,
);
}
}
}

View File

@@ -62,59 +62,6 @@
<h2 class="text-xl font-semibold">I want to know more because...</h2>
<ul class="list-disc list-outside ml-4">
<li class="p-2">
<div class="text-blue-500" @click="toggleAlpha">... I'm a member of Alpha chat.</div>
<div v-if="showAlpha">
<p>
This is a project for public benefit. You are invited to add your gratitude
and propose projects on a distributable ledger.
</p>
<p>
The underlying data is on a merkle tree with each verifiable claim, signature and all.
The chain includes individual IDs for discovery & visibility, so not all data is distributed -- yet.
The goal is to eventually distribute the data on people's devices with their chosen network,
where anyone could host their own chain of provenance if they choose.
The formats follow standard schemas (eg. schema.org) to encourage interoperability.
We're currently at the beginning phase where we're trusting the server to keep IDs private.
It's all open-source, and we expect to have a professional audit someday.
</p>
<p>
A person's network of contacts is similar: the server currently knows some of the links between people
to allow discovery and visibility. However, even that will be manageable on personal devices someday.
</p>
<p>
There are no tokens to maintain the chain: the purpose is to create software that communities
and activists can easily join and use. We're betting that this is a case where network
participants have the motivation to run the software. The protocol is meant to be lightweight enough that
non-technical people can run it on inexpensive devices they already own. There may be cases for
MPC or ZKP in the future when they are more widespread and standard,
but our preference is to engineer as simply as possible with "white-magic" cryptography
over those "black-magic" functions.
</p>
<p>
Let's make real distributed computing and shared data happen, starting with our own small networks.
</p>
<p>
... and exemplify the fun along the way.
</p>
</div>
</li>
<li class="p-2">
<div class="text-blue-500" @click="toggleGroup">... I want to find a group I'll enjoy working with.</div>
<div v-if="showGroup">
<p>
This app encourages people to offer small bits of time to one another. It's a way to
run experiments with other people... tests of working together, which can start small
and easy but build into cooperation with people who are like-minded and who work well together.
</p>
<p>
Search the projects and place an offer on an interesting one
-- or create your own project and see who offers to help.
After your first experiment, you can give and get confirmation about the work, which you might choose
to show to future contacts.
</p>
</div>
</li>
<li class="p-2">
<div class="text-blue-500" @click="toggleCommunity">... I want to participate in community projects.</div>
<div v-if="showCommunity">
@@ -188,6 +135,59 @@
</p>
</div>
</li>
<li class="p-2">
<div class="text-blue-500" @click="toggleGroup">... I want to find a group I'll enjoy working with.</div>
<div v-if="showGroup">
<p>
This app encourages people to offer small bits of time to one another. It's a way to
run experiments with other people... tests of working together, which can start small
and easy but build into cooperation with people who are like-minded and who work well together.
</p>
<p>
Search the projects and place an offer on an interesting one
-- or create your own project and see who offers to help.
After your first experiment, you can give and get confirmation about the work, which you might choose
to show to future contacts.
</p>
</div>
</li>
<li class="p-2">
<div class="text-blue-500" @click="toggleAlpha">... I'm a member of Alpha chat.</div>
<div v-if="showAlpha">
<p>
This is a project for public benefit. You are invited to add your gratitude
and propose projects on a distributable ledger.
</p>
<p>
The underlying data is on a merkle tree with each verifiable claim, signature and all.
The chain includes individual IDs for discovery & visibility, so not all data is distributed -- yet.
The goal is to eventually distribute the data on people's devices with their chosen network,
where anyone could host their own chain of provenance if they choose.
The formats follow standard schemas (eg. schema.org) to encourage interoperability.
We're currently at the beginning phase where we're trusting the server to keep IDs private.
It's all open-source, and we expect to have a professional audit someday.
</p>
<p>
A person's network of contacts is similar: the server currently knows some of the links between people
to allow discovery and visibility. However, even that will be manageable on personal devices someday.
</p>
<p>
There are no tokens to maintain the chain: the purpose is to create software that communities
and activists can easily join and use. We're betting that this is a case where network
participants have the motivation to run the software. The protocol is meant to be lightweight enough that
non-technical people can run it on inexpensive devices they already own. There may be cases for
MPC or ZKP in the future when they are more widespread and standard,
but our preference is to engineer as simply as possible with "white-magic" cryptography
over those "black-magic" functions.
</p>
<p>
Let's make real distributed computing and shared data happen, starting with our own small networks.
</p>
<p>
... and exemplify the fun along the way.
</p>
</div>
</li>
</ul>
<h2 class="text-xl font-semibold">How do I get started?</h2>

View File

@@ -234,11 +234,9 @@ Raymer * @version 1.0.0 */
:last-viewed-claim-id="feedLastViewedClaimId"
:is-registered="isRegistered"
:active-did="activeDid"
:confirmer-id-list="record.confirmerIdList"
:on-image-cache="cacheImageData"
@load-claim="onClickLoadClaim"
@view-image="openImageViewer"
@confirm-claim="confirmClaim"
/>
</ul>
</InfiniteScroll>
@@ -306,15 +304,11 @@ import {
OnboardPage,
} from "../libs/util";
import { GiveSummaryRecord } from "../interfaces/records";
import * as serverUtil from "../libs/endorserServer";
import { logger } from "../utils/logger";
import { GiveRecordWithContactInfo } from "../interfaces/give";
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
import {
NOTIFY_CONTACT_LOADING_ISSUE,
NOTIFY_CONFIRMATION_ERROR,
} from "@/constants/notifications";
import { NOTIFY_CONTACT_LOADING_ISSUE } from "@/constants/notifications";
import * as Package from "../../package.json";
// consolidate this with GiveActionClaim in src/interfaces/claims.ts
@@ -640,42 +634,6 @@ export default class HomeView extends Vue {
}
}
/**
* Loads user settings from database using ultra-concise mixin
* Used for displaying settings in feed and actions
*
* @internal
* Called by mounted() and reloadFeedOnChange()
*/
private async loadSettings() {
// Use the current activeDid (set in initializeIdentity) to get user-specific settings
const settings = await this.$accountSettings(this.activeDid, {
apiServer: "",
activeDid: "",
filterFeedByVisible: false,
filterFeedByNearby: false,
isRegistered: false,
});
this.apiServer = settings.apiServer || "";
// **CRITICAL**: Ensure correct API server for platform
await this.ensureCorrectApiServer();
this.activeDid = settings.activeDid || "";
this.feedLastViewedClaimId = settings.lastViewedClaimId;
this.givenName = settings.firstName || "";
this.isFeedFilteredByVisible = !!settings.filterFeedByVisible;
this.isFeedFilteredByNearby = !!settings.filterFeedByNearby;
this.isRegistered = !!settings.isRegistered;
this.lastAckedOfferToUserJwtId = settings.lastAckedOfferToUserJwtId;
this.lastAckedOfferToUserProjectsJwtId =
settings.lastAckedOfferToUserProjectsJwtId;
this.searchBoxes = settings.searchBoxes || [];
this.showShortcutBvc = !!settings.showShortcutBvc;
this.isAnyFeedFilterOn = checkIsAnyFeedFilterOn(settings);
}
/**
* Loads user contacts from database using ultra-concise mixin
* Used for displaying contact info in feed and actions
@@ -690,36 +648,6 @@ export default class HomeView extends Vue {
.map((c) => c.did);
}
/**
* Verifies user registration status with endorser service
* - Checks if unregistered user can access API
* - Updates registration status if successful
* - Preserves unregistered state on failure
*
* @internal
* Called by mounted() and initializeIdentity()
*/
private async checkRegistrationStatus() {
if (!this.isRegistered && this.activeDid) {
try {
const resp = await fetchEndorserRateLimits(
this.apiServer,
this.axios,
this.activeDid,
);
if (resp.status === 200) {
// Ultra-concise settings update with automatic cache invalidation!
await this.$saveMySettings({ isRegistered: true });
this.isRegistered = true;
// Force Vue to re-render the template
await this.$nextTick();
}
} catch (e) {
// ignore the error... just keep us unregistered
}
}
}
/**
* Initializes feed data
* Triggers updateAllFeed() to populate activity feed
@@ -1716,53 +1644,6 @@ export default class HomeView extends Vue {
this.isImageViewerOpen = true;
}
/**
* Handles claim confirmation
*
* @public
* Called by ActivityListItem component
* @param record Record to confirm
*/
async confirmClaim(record: GiveRecordWithContactInfo) {
this.notify.confirm(
"Do you personally confirm that this is true?",
async () => {
const goodClaim = serverUtil.removeSchemaContext(
serverUtil.removeVisibleToDids(
serverUtil.addLastClaimOrHandleAsIdIfMissing(
record.fullClaim,
record.jwtId,
record.handleId,
),
),
);
const confirmationClaim = {
"@context": "https://schema.org",
"@type": "AgreeAction",
object: goodClaim,
};
const result = await serverUtil.createAndSubmitClaim(
confirmationClaim,
this.activeDid,
this.apiServer,
this.axios,
);
if (result.success) {
this.notify.confirmationSubmitted();
// Refresh the feed to show updated confirmation status
await this.updateAllFeed();
} else {
logger.error("Error submitting confirmation:", result);
this.notify.error(NOTIFY_CONFIRMATION_ERROR.message, TIMEOUTS.LONG);
}
},
);
}
private handleQRCodeClick() {
if (Capacitor.isNativePlatform()) {
this.$router.push({ name: "contact-qr-scan-full" });

View File

@@ -115,6 +115,8 @@ import {
serverMessageForUser,
} from "../libs/endorserServer";
import { PlatformServiceMixin } from "@/utils/PlatformServiceMixin";
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
import { NotificationIface } from "@/constants/app";
interface Meeting {
name: string;
@@ -129,19 +131,11 @@ interface Meeting {
mixins: [PlatformServiceMixin],
})
export default class OnboardMeetingListView extends Vue {
$notify!: (
notification: {
group: string;
type: string;
title: string;
text: string;
onYes?: () => void;
yesText?: string;
},
timeout?: number,
) => void;
$notify!: (notification: NotificationIface, timeout?: number) => void;
$router!: Router;
notify!: ReturnType<typeof createNotifyHelpers>;
activeDid = "";
apiServer = "";
attendingMeeting: Meeting | null = null;
@@ -153,30 +147,66 @@ export default class OnboardMeetingListView extends Vue {
selectedMeeting: Meeting | null = null;
showPasswordDialog = false;
/**
* Vue lifecycle hook - component initialization
*
* Initializes the component by loading user settings and fetching available
* onboarding meetings. This method is called when the component is created
* and sets up all necessary data for the meeting list interface.
*
* Workflow:
* 1. Initialize notification system using createNotifyHelpers
* 2. Load user account settings (DID, API server, registration status)
* 3. Fetch available onboarding meetings from the server
*
* Dependencies:
* - PlatformServiceMixin for settings access ($accountSettings)
* - Server API for meeting data (fetchMeetings)
*
* Error Handling:
* - Server errors during meeting fetch are handled in fetchMeetings()
*
* @author Matthew Raymer
*/
async created() {
const settings = await this.$accountSettings();
this.notify = createNotifyHelpers(this.$notify);
if (settings?.activeDid) {
try {
// Verify database settings are accessible
await this.$query("SELECT * FROM settings WHERE accountDid = ?", [
settings.activeDid,
]);
} catch (error) {
logger.error("Error checking database settings:", error);
}
}
// Load user account settings
const settings = await this.$accountSettings();
this.activeDid = settings?.activeDid || "";
this.apiServer = settings?.apiServer || "";
this.firstName = settings?.firstName || "";
this.isRegistered = !!settings?.isRegistered;
if (this.isRegistered) {
await this.fetchMeetings();
}
await this.fetchMeetings();
}
/**
* Fetches available onboarding meetings from the server
*
* This method retrieves the list of onboarding meetings that the user can join.
* It first checks if the user is already attending a meeting, and if so,
* displays that meeting instead of the full list.
*
* Workflow:
* 1. Check if user is already attending a meeting (groupOnboardMember endpoint)
* 2. If attending: Fetch meeting details and display single meeting view
* 3. If not attending: Fetch all available meetings (groupsOnboarding endpoint)
* 4. Handle loading states and error conditions
*
* API Endpoints Used:
* - GET /api/partner/groupOnboardMember - Check current attendance
* - GET /api/partner/groupOnboard/{id} - Get meeting details
* - GET /api/partner/groupsOnboarding - Get all available meetings
*
* State Management:
* - Sets isLoading flag during API calls
* - Updates attendingMeeting or meetings array
* - Handles error states with user notifications
*
* @author Matthew Raymer
*/
async fetchMeetings() {
this.isLoading = true;
try {
@@ -226,20 +256,36 @@ export default class OnboardMeetingListView extends Vue {
"Error fetching meetings: " + errorStringForLog(error),
true,
);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: serverMessageForUser(error) || "Failed to fetch meetings.",
},
5000,
this.notify.error(
serverMessageForUser(error) || "There was a problem fetching meetings.",
TIMEOUTS.LONG,
);
} finally {
this.isLoading = false;
}
}
/**
* Opens the password dialog for joining a meeting
*
* This method initiates the process of joining an onboarding meeting by
* opening a modal dialog that prompts the user for the meeting password.
* The dialog is focused and ready for input when displayed.
*
* Workflow:
* 1. Clear any previous password input
* 2. Store the selected meeting for later use
* 3. Show the password dialog modal
* 4. Focus the password input field for immediate typing
*
* UI State Changes:
* - Sets showPasswordDialog to true (shows modal)
* - Clears password field for fresh input
* - Stores selectedMeeting for password submission
*
* @param meeting - The meeting object the user wants to join
* @author Matthew Raymer
*/
promptPassword(meeting: Meeting) {
this.password = "";
this.selectedMeeting = meeting;
@@ -252,12 +298,61 @@ export default class OnboardMeetingListView extends Vue {
});
}
/**
* Cancels the password dialog and resets state
*
* This method handles the cancellation of the meeting password dialog.
* It cleans up the dialog state and resets all related variables to
* their initial state, ensuring a clean slate for future dialog interactions.
*
* State Cleanup:
* - Clears password input field
* - Removes selected meeting reference
* - Hides password dialog modal
*
* This ensures that if the user reopens the dialog, they start with
* a fresh state without any leftover data from previous attempts.
*
* @author Matthew Raymer
*/
cancelPasswordDialog() {
this.password = "";
this.selectedMeeting = null;
this.showPasswordDialog = false;
}
/**
* Submits the password and joins the selected meeting
*
* This method handles the complete workflow of joining an onboarding meeting.
* It encrypts the user's member data with the provided password and sends
* it to the server to register the user as a meeting participant.
*
* Workflow:
* 1. Validate that a meeting is selected (safety check)
* 2. Create member data object with user information
* 3. Encrypt member data using the meeting password
* 4. Send encrypted data to server via groupOnboardMember endpoint
* 5. On success: Navigate to meeting members view with credentials
* 6. On failure: Show error notification to user
*
* Data Encryption:
* - Member data includes: name, DID, registration status
* - Data is encrypted using the meeting password for security
* - Encrypted data is sent to server for verification
*
* Navigation:
* - On successful join: Redirects to onboard-meeting-members view
* - Passes groupId, password, and memberId as route parameters
* - Allows user to see other meeting participants
*
* Error Handling:
* - Invalid passwords result in server rejection
* - Network errors are caught and displayed to user
* - All errors are logged for debugging purposes
*
* @author Matthew Raymer
*/
async submitPassword() {
if (!this.selectedMeeting) {
// this should never happen
@@ -316,69 +411,95 @@ export default class OnboardMeetingListView extends Vue {
"Error joining meeting: " + errorStringForLog(error),
true,
);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text:
serverMessageForUser(error) || "You failed to join the meeting.",
},
5000,
this.notify.error(
serverMessageForUser(error) ||
"There was a problem joining the meeting.",
TIMEOUTS.LONG,
);
}
}
/**
* Prompts user to confirm leaving the current meeting
*
* This method initiates the process of leaving an onboarding meeting.
* It shows a confirmation dialog to prevent accidental departures,
* then handles the server-side removal and UI updates.
*
* Workflow:
* 1. Display confirmation dialog asking user to confirm departure
* 2. On confirmation: Send DELETE request to groupOnboardMember endpoint
* 3. On success: Clear attending meeting state and refresh meeting list
* 4. Show success notification to user
* 5. On failure: Show error notification with details
*
* Server Interaction:
* - DELETE /api/partner/groupOnboardMember - Removes user from meeting
* - Requires authentication headers for user verification
* - Server handles the actual removal from meeting database
*
* State Management:
* - Clears attendingMeeting when successfully left
* - Refreshes meetings list to show updated availability
* - Updates UI to show meeting list instead of single meeting
*
* User Experience:
* - Confirmation prevents accidental departures
* - Clear feedback on success/failure
* - Seamless transition back to meeting list
*
* @author Matthew Raymer
*/
async leaveMeeting() {
this.$notify(
{
group: "modal",
type: "confirm",
title: "Leave Meeting",
text: "Are you sure you want to leave this meeting?",
onYes: async () => {
try {
const headers = await getHeaders(this.activeDid);
await this.axios.delete(
this.apiServer + "/api/partner/groupOnboardMember",
{ headers },
);
this.notify.confirm(
"Are you sure you want to leave this meeting?",
async () => {
try {
const headers = await getHeaders(this.activeDid);
await this.axios.delete(
this.apiServer + "/api/partner/groupOnboardMember",
{ headers },
);
this.attendingMeeting = null;
await this.fetchMeetings();
this.attendingMeeting = null;
await this.fetchMeetings();
this.$notify(
{
group: "alert",
type: "success",
title: "Success",
text: "You left the meeting.",
},
5000,
);
} catch (error) {
this.$logAndConsole(
"Error leaving meeting: " + errorStringForLog(error),
true,
);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text:
serverMessageForUser(error) ||
"You failed to leave the meeting.",
},
5000,
);
}
},
this.notify.success("You left the meeting.", TIMEOUTS.LONG);
} catch (error) {
this.$logAndConsole(
"Error leaving meeting: " + errorStringForLog(error),
true,
);
this.notify.error(
serverMessageForUser(error) ||
"There was a problem leaving the meeting.",
TIMEOUTS.LONG,
);
}
},
-1,
);
}
/**
* Navigates to the meeting creation page
*
* This method handles the navigation to the meeting setup page where
* registered users can create new onboarding meetings. It's only
* available to users who are registered in the system.
*
* Navigation:
* - Routes to onboard-meeting-setup view
* - Allows user to configure new meeting settings
* - Only accessible to registered users (controlled by template)
*
* User Flow:
* - User clicks "Create Meeting" button
* - System navigates to setup page
* - User can configure meeting name, password, etc.
* - New meeting becomes available to other users
*
* @author Matthew Raymer
*/
createMeeting() {
this.$router.push({ name: "onboard-meeting-setup" });
}

View File

@@ -75,15 +75,9 @@ export default class ShareMyContactInfoView extends Vue {
isLoading = false;
async mounted() {
// Debug logging for test diagnosis
const settings = await this.$settings();
const activeDid = settings?.activeDid;
// @ts-expect-error - Debug property for testing contact sharing functionality
window.__SHARE_CONTACT_DEBUG__ = { settings, activeDid };
// eslint-disable-next-line no-console
if (!activeDid) {
// eslint-disable-next-line no-console
this.$router.push({ name: "home" });
}
}

View File

@@ -6,7 +6,9 @@ import path from "path";
import { fileURLToPath } from 'url';
// Load environment variables
dotenv.config();
console.log('NODE_ENV:', process.env.NODE_ENV)
dotenv.config({ path: `.env.${process.env.NODE_ENV}` })
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);