Compare commits

..

1 Commits

Author SHA1 Message Date
Matt Raymer
d402642db8 feat: add centralized git hash retrieval script
- Create scripts/get-git-hash.sh for consistent git hash logic
- Update build scripts to use new centralized git hash function
- Add comprehensive documentation and usage examples
- Ensure branch-aware hash retrieval across all build types

Provides single source of truth for git hash retrieval, preventing inconsistencies
when builds use wrong branch's commit hash.
2025-08-07 08:56:07 -04:00
30 changed files with 1123 additions and 3326 deletions

View File

@@ -1,122 +1,34 @@
---
alwaysApply: true
---
# Directive: Peaceful Co-Existence with Developers
# Rules for peaceful co-existence with developers
## 1) Version-Control Ownership
do not add or commit for the user; let him control that process
* **MUST NOT** run `git add`, `git commit`, or any write action.
* **MUST** leave staging/committing to the developer.
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
## 2) Source of Truth for Commit Text
always preview changes and commit message to use and allow me to copy and paste
✅ Preferred Commit Message Format
* **MUST** derive messages **only** from:
Short summary in the first line (concise and high-level).
Avoid long commit bodies unless truly necessary.
* 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.
✅ Valued Content in Commit Messages
## 3) Mandatory Preview Flow
Specific fixes or features.
Symptoms or problems that were fixed.
Notes about tests passing or TS/linting errors being resolved (briefly).
* **ALWAYS** present, before any real commit:
❌ Avoid in Commit Messages
* file list + brief per-file notes,
* a **draft commit message** (copy-paste ready),
* nothing auto-applied.
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.
---
Guiding Principle
# 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
Let code and inline documentation speak for themselves. Use commits to highlight what isn't obvious from reading the code.

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 39 && perl -p -i -e "s/MARKETING_VERSION = .*;/MARKETING_VERSION = 1.0.6;/g" App.xcodeproj/project.pbxproj && cd -
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 -
# Unfortunately this edits Info.plist directly.
#xcrun agvtool new-marketing-version 0.4.5
```
@@ -1063,12 +1063,11 @@ 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. You may have to 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: 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,27 +12,9 @@ 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
## [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
## [Unreleased]
### 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 fontawesome.ts and reference with `font-awesome` element and `icon` attribute with the hyphenated name.
To add a Font Awesome icon, add to main.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 39
versionName "1.0.6"
versionCode 35
versionName "1.0.2"
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 = 39;
CURRENT_PROJECT_VERSION = 37;
DEVELOPMENT_TEAM = GM3FS5JQPH;
ENABLE_APP_SANDBOX = NO;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
@@ -413,7 +413,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0.6;
MARKETING_VERSION = 1.0.4;
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 = 39;
CURRENT_PROJECT_VERSION = 37;
DEVELOPMENT_TEAM = GM3FS5JQPH;
ENABLE_APP_SANDBOX = NO;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
@@ -440,7 +440,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0.6;
MARKETING_VERSION = 1.0.4;
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=`git log -1 --pretty=format:%h` vite build --mode capacitor --config vite.config.capacitor.mts",
"build:capacitor": "VITE_GIT_HASH=$(./scripts/get-git-hash.sh) 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,7 +168,11 @@ build_web_assets() {
local mode=$1
log_info "Building web assets for Electron (mode: $mode)"
safe_execute "Building web assets" "VITE_GIT_HASH=\$(git log -1 --pretty=format:%h) vite build --mode $mode --config vite.config.electron.mts"
# 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"
}
# Sync with Capacitor

View File

@@ -203,8 +203,12 @@ 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 log -1 --pretty=format:%h) npx vite build --config vite.config.web.mts"
local vite_cmd="VITE_GIT_HASH=$git_hash npx vite build --config vite.config.web.mts"
# Add mode if not development (development is default)
if [ "$mode" != "development" ]; then
@@ -275,8 +279,12 @@ 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 log -1 --pretty=format:%h) npx vite --config vite.config.web.mts"
local vite_cmd="VITE_GIT_HASH=$git_hash npx vite --config vite.config.web.mts"
# Add mode if specified (though development is default)
if [ "$BUILD_MODE" != "development" ]; then

View File

@@ -134,10 +134,25 @@ check_venv() {
# Function to get git hash for versioning
get_git_hash() {
if command -v git &> /dev/null; then
git log -1 --pretty=format:%h 2>/dev/null || echo "unknown"
# Use the dedicated git hash script for consistency
if [ -f "$(dirname "$0")/get-git-hash.sh" ]; then
"$(dirname "$0")/get-git-hash.sh"
else
echo "unknown"
# 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
fi
}

105
scripts/get-git-hash.sh Executable file
View File

@@ -0,0 +1,105 @@
#!/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,7 +251,8 @@
import { Component, Prop, Vue, Emit } from "vue-facing-decorator";
import { GiveRecordWithContactInfo } from "@/interfaces/give";
import EntityIcon from "./EntityIcon.vue";
import { isHiddenDid } from "../libs/endorserServer";
import { isGiveClaimType, notifyWhyCannotConfirm } from "../libs/util";
import { containsHiddenDid, isHiddenDid } from "../libs/endorserServer";
import ProjectIcon from "./ProjectIcon.vue";
import { createNotifyHelpers } from "@/utils/notify";
import {
@@ -271,6 +272,7 @@ export default class ActivityListItem extends Vue {
@Prop() lastViewedClaimId?: string;
@Prop() isRegistered!: boolean;
@Prop() activeDid!: string;
@Prop() confirmerIdList?: string[];
/**
* Function prop for handling image caching
@@ -329,6 +331,15 @@ 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) {
@@ -340,6 +351,26 @@ 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,10 +54,7 @@ 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";
@@ -182,19 +179,7 @@ export default class DataExportSection extends Vue {
const allContacts = await this.$contacts();
// Convert contacts to export format
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 exportData = contactsToExportJson(allContacts);
const jsonStr = JSON.stringify(exportData, null, 2);
// Use platform service to handle export (no platform-specific logic here!)

View File

@@ -15,25 +15,26 @@ 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 mb-4">
<AmountInput
:value="parseFloat(amountInput) || 0"
:onUpdateValue="handleAmountUpdate"
data-testId="inputOfferAmount"
/>
<select
v-model="amountUnitCode"
class="flex-1 rounded border border-slate-400 ms-2 px-3 py-2"
<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()"
>
<option
v-for="(displayName, code) in unitOptions"
:key="code"
:value="code"
>
{{ displayName }}
</option>
</select>
<font-awesome icon="chevron-left" />
</div>
<input
v-model="amountInput"
data-testId="inputOfferAmount"
type="number"
:class="amountInputClasses"
/>
<div :class="incrementButtonClasses" @click="increment()">
<font-awesome icon="chevron-right" />
</div>
</div>
<div class="mt-4 flex justify-center">
<span>
@@ -72,15 +73,10 @@ 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;
@@ -126,10 +122,35 @@ export default class OfferDialog extends Vue {
}
/**
* Computed property to get unit options for the select dropdown
* CSS classes for unit code selector and increment/decrement buttons
* Reduces template complexity for repeated border and styling patterns
*/
get unitOptions() {
return this.libsUtil.UNIT_SHORT;
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";
}
/**
@@ -152,7 +173,13 @@ 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
@@ -199,14 +226,30 @@ 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];
}
/**
* Handle amount updates from AmountInput component
* @param value - New amount value
* Increment the amount input
*/
handleAmountUpdate(value: number) {
this.amountInput = value.toString();
increment() {
this.amountInput = `${(parseFloat(this.amountInput) || 0) + 1}`;
}
/**
* Decrement the amount input
*/
decrement() {
this.amountInput = `${Math.max(
0,
(parseFloat(this.amountInput) || 1) - 1,
)}`;
}
/**
@@ -230,28 +273,6 @@ 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);
@@ -280,6 +301,20 @@ 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,
@@ -328,7 +363,7 @@ export default class OfferDialog extends Vue {
display: flex;
justify-content: center;
align-items: center;
z-index: 50;
z-index: 1000;
}
.dialog {

View File

@@ -33,6 +33,7 @@ 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 export JSON format.
* Converts an array of contacts to the standardized database export JSON format.
* This format is used for data migration and backup purposes.
*
* @param contacts - Array of Contact objects to convert

View File

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

View File

@@ -149,10 +149,6 @@ 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 };
}
@@ -199,14 +195,14 @@ export class DeepLinkHandler {
// Continue with parameter validation as before...
const schema = deepLinkSchemas[path as keyof typeof deepLinkSchemas];
let validatedParams;
let validatedParams, validatedQuery;
try {
validatedParams = await schema.parseAsync(params);
validatedQuery = await schema.parseAsync(query);
} catch (error) {
// For parameter validation errors, provide specific error feedback
logConsoleAndDb(
console.error(
`[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",
@@ -227,11 +223,11 @@ export class DeepLinkHandler {
await this.router.replace({
name: routeName,
params: validatedParams,
query: validatedQuery,
});
} catch (error) {
logConsoleAndDb(
`[DeepLink] Error routing to route name ${routeName} for path: ${path}: ${JSON.stringify(error)} ... with validated params: ${JSON.stringify(validatedParams)}`,
true,
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)}`,
);
// For parameter validation errors, provide specific error feedback
await this.router.replace({
@@ -241,6 +237,7 @@ export class DeepLinkHandler {
originalPath: path,
errorCode: "ROUTING_ERROR",
errorMessage: `Error routing to ${routeName}: ${JSON.stringify(error)}`,
...validatedQuery,
},
});
}
@@ -263,9 +260,8 @@ export class DeepLinkHandler {
await this.validateAndRoute(path, sanitizedParams, query);
} catch (error) {
const deepLinkError = error as DeepLinkError;
logConsoleAndDb(
console.error(
`[DeepLink] Error (${deepLinkError.code}): ${deepLinkError.details}`,
true,
);
throw {

View File

@@ -60,10 +60,8 @@
@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="false"
v-if="isRegistered"
id="sectionNotifications"
class="bg-slate-100 rounded-md overflow-hidden px-4 py-4 mt-8 mb-8"
aria-labelledby="notificationsHeading"

View File

@@ -52,6 +52,7 @@ 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

@@ -553,6 +553,11 @@ 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,11 +213,15 @@ export default class ContactQRScanShow extends Vue {
$router!: Router;
// Notification helper system
notify!: ReturnType<typeof createNotifyHelpers>;
private notify = createNotifyHelpers(this.$notify);
activeDid = "";
apiServer = "";
// Axios instance for API calls
get axios() {
return (this as any).$platformService.axios;
}
givenName = "";
hideRegisterPromptOnNewContact = false;
isRegistered = false;
@@ -284,8 +288,6 @@ 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,7 +273,6 @@ import {
displayAmount,
getHeaders,
register,
setVisibilityUtil,
} from "../libs/endorserServer";
import * as libsUtil from "../libs/util";
import EntityIcon from "../components/EntityIcon.vue";
@@ -325,7 +324,6 @@ export default class DIDView extends Vue {
apiServer = "";
claims: Array<GenericCredWrapper<GenericVerifiableCredential>> = [];
contactFromDid?: Contact;
contactYaml = "";
hitEnd = false;
isLoading = false;
@@ -724,31 +722,18 @@ export default class DIDView extends Vue {
visibility: boolean,
showSuccessAlert: boolean,
) {
const result = await setVisibilityUtil(
this.activeDid,
this.apiServer,
this.axios,
contact,
visibility,
);
// Update contact visibility using mixin method
await this.$updateContact(contact.did, { seesMe: visibility });
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);
if (showSuccessAlert) {
const message =
(result.error as string) || "Could not set visibility on the server.";
this.notify.error(message, TIMEOUTS.LONG);
return false;
(contact.name || "That user") +
" can " +
(visibility ? "" : "not ") +
"see your activity.";
this.notify.success(message, TIMEOUTS.SHORT);
}
return true;
}
/**

View File

@@ -588,20 +588,10 @@ export default class GiftedDetails extends Vue {
this.imageUrl = "";
} catch (error) {
logger.error("Error deleting image:", error);
// 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,
);
}
this.notify.error(
NOTIFY_GIFTED_DETAILS_DELETE_IMAGE_ERROR.message,
TIMEOUTS.LONG,
);
}
}

View File

@@ -62,6 +62,59 @@
<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">
@@ -135,59 +188,6 @@
</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,9 +234,11 @@ 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>
@@ -304,11 +306,15 @@ 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 } from "@/constants/notifications";
import {
NOTIFY_CONTACT_LOADING_ISSUE,
NOTIFY_CONFIRMATION_ERROR,
} from "@/constants/notifications";
import * as Package from "../../package.json";
// consolidate this with GiveActionClaim in src/interfaces/claims.ts
@@ -634,6 +640,42 @@ 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
@@ -648,6 +690,36 @@ 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
@@ -1644,6 +1716,53 @@ 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,8 +115,6 @@ 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;
@@ -131,11 +129,19 @@ interface Meeting {
mixins: [PlatformServiceMixin],
})
export default class OnboardMeetingListView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
$notify!: (
notification: {
group: string;
type: string;
title: string;
text: string;
onYes?: () => void;
yesText?: string;
},
timeout?: number,
) => void;
$router!: Router;
notify!: ReturnType<typeof createNotifyHelpers>;
activeDid = "";
apiServer = "";
attendingMeeting: Meeting | null = null;
@@ -147,66 +153,30 @@ 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() {
this.notify = createNotifyHelpers(this.$notify);
// Load user account settings
const settings = await this.$accountSettings();
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);
}
}
this.activeDid = settings?.activeDid || "";
this.apiServer = settings?.apiServer || "";
this.firstName = settings?.firstName || "";
this.isRegistered = !!settings?.isRegistered;
await this.fetchMeetings();
if (this.isRegistered) {
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 {
@@ -256,36 +226,20 @@ export default class OnboardMeetingListView extends Vue {
"Error fetching meetings: " + errorStringForLog(error),
true,
);
this.notify.error(
serverMessageForUser(error) || "There was a problem fetching meetings.",
TIMEOUTS.LONG,
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: serverMessageForUser(error) || "Failed to fetch meetings.",
},
5000,
);
} 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;
@@ -298,61 +252,12 @@ 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
@@ -411,95 +316,69 @@ export default class OnboardMeetingListView extends Vue {
"Error joining meeting: " + errorStringForLog(error),
true,
);
this.notify.error(
serverMessageForUser(error) ||
"There was a problem joining the meeting.",
TIMEOUTS.LONG,
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text:
serverMessageForUser(error) || "You failed to join the meeting.",
},
5000,
);
}
}
/**
* 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.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.$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.attendingMeeting = null;
await this.fetchMeetings();
this.attendingMeeting = null;
await this.fetchMeetings();
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,
);
}
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,
);
}
},
},
-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,9 +75,15 @@ 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,9 +6,7 @@ import path from "path";
import { fileURLToPath } from 'url';
// Load environment variables
console.log('NODE_ENV:', process.env.NODE_ENV)
dotenv.config({ path: `.env.${process.env.NODE_ENV}` })
dotenv.config();
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);